From e03748c77cb5e95fe67350dbd9727d7d9b63e1c8 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 19 Feb 2026 07:10:58 -0500 Subject: [PATCH 001/261] [ty] Add warning message when running `ty server` interactively Closes astral-sh/ty#2851 --- crates/ty_server/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs index b0916fbe28a2b..fe7ba186c6fbe 100644 --- a/crates/ty_server/src/lib.rs +++ b/crates/ty_server/src/lib.rs @@ -27,6 +27,7 @@ pub(crate) const DIAGNOSTIC_NAME: &str = "ty"; pub(crate) type Result = anyhow::Result; pub fn run_server() -> anyhow::Result<()> { + let _ = print_interactive_warning(); let four = NonZeroUsize::new(4).unwrap(); // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. @@ -71,3 +72,20 @@ pub fn run_server() -> anyhow::Result<()> { result } + +fn print_interactive_warning() -> std::io::Result<()> { + use std::io::{IsTerminal, Write}; + + if std::io::stdin().is_terminal() { + let mut stderr = std::io::stderr().lock(); + writeln!( + stderr, + "WARNING: the ty LSP server should not be run interactively" + )?; + writeln!( + stderr, + "See https://docs.astral.sh/ty/editors/ for how to configure your editor" + )?; + } + Ok(()) +} From 6551438942e494f6be1aaef0dd01ba65a4c222ef Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 19 Feb 2026 18:22:14 +0530 Subject: [PATCH 002/261] [parser] Fix indentation tracking after line continuations (#23417) ## Summary fixes: https://github.com/astral-sh/ruff/issues/19301 ## Test Plan Add new lexer and parser test cases. --------- Co-authored-by: Kartik Ganapathi --- ...ackslash_continuation_indentation_error.py | 4 + .../ok/backslash_continuation_indentation.py | 7 ++ crates/ruff_python_parser/src/lexer.rs | 97 ++++++++++++++++++- ...tests__backslash_continuation_at_root.snap | 95 ++++++++++++++++++ ...s__backslash_continuation_indentation.snap | 91 +++++++++++++++++ ...ash_continuation_mismatch_indentation.snap | 62 ++++++++++++ ...ests__multiple_backslash_continuation.snap | 53 ++++++++++ ...ash_continuation_indentation_error.py.snap | 77 +++++++++++++++ ...backslash_continuation_indentation.py.snap | 84 ++++++++++++++++ 9 files changed, 569 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/backslash_continuation_indentation_error.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/backslash_continuation_indentation.py create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@backslash_continuation_indentation.py.snap diff --git a/crates/ruff_python_parser/resources/inline/err/backslash_continuation_indentation_error.py b/crates/ruff_python_parser/resources/inline/err/backslash_continuation_indentation_error.py new file mode 100644 index 0000000000000..e9b294f49fc96 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/backslash_continuation_indentation_error.py @@ -0,0 +1,4 @@ +if True: + 1 + \ + 2 diff --git a/crates/ruff_python_parser/resources/inline/ok/backslash_continuation_indentation.py b/crates/ruff_python_parser/resources/inline/ok/backslash_continuation_indentation.py new file mode 100644 index 0000000000000..f9b9d7500996b --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/backslash_continuation_indentation.py @@ -0,0 +1,7 @@ +if True: + \ + 1 + \ +2 +else:\ + 3 diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index 8b4b3a061c47d..31f4342d75570 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -263,7 +263,35 @@ impl<'src> Lexer<'src> { self.token_range(), ))); } - indentation = Indentation::root(); + // test_ok backslash_continuation_indentation + // if True: + // \ + // 1 + // \ + // 2 + // else:\ + // 3 + + // test_err backslash_continuation_indentation_error + // if True: + // 1 + // \ + // 2 + + // > Indentation cannot be split over multiple physical lines using backslashes; + // > the whitespace up to the first backslash determines the indentation. + // > + // > https://docs.python.org/3/reference/lexical_analysis.html#indentation + // + // Skip whitespace after the continuation-line without accumulating it into + // `indentation`. However, if the backslash is at column 0 (no prior + // indentation), let the loop continue so the next line's whitespace is + // accumulated normally. + // + // See also: https://github.com/python/cpython/issues/90249 + if indentation != Indentation::root() { + self.cursor.eat_while(is_python_whitespace); + } } // Form feed '\x0C' => { @@ -3061,4 +3089,71 @@ t"{(lambda x:{x})}" UnterminatedTripleQuotedString ); } + + #[test] + fn backslash_continuation_indentation() { + // The first `\` has 4 spaces before it which matches the indentation level at that point, + // so the whitespace before `2` is irrelevant and shouldn't produce an indentation error. + // Similarly, the second `\` is also at the same indentation level, so the `3` line is also + // valid. + let source = r"if True: + 1 + \ + 2 + \ +3 +else: + pass +" + .to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn backslash_continuation_at_root() { + // But, it's a different when the backslash character itself is at the root indentation + // level. Then, the whitespaces following it determines the indentation level of the next + // line, so `1` is indented with 4 spaces and `2` is indented with 8 spaces, and `3` is + // indented with 4 spaces, all of which are valid. + let source = r"if True: +\ + 1 + if True: +\ + 2 +else:\ + 3 +" + .to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn multiple_backslash_continuation() { + // It's only the first backslash character that determines the indentation level of the next + // line, so all the lines after the first `\` are indented with 4 spaces, and the remaining + // backslashes are just ignored and don't affect the indentation level. + let source = r"if True: + 1 + \ + \ + \ + \ + 2 +" + .to_string(); + assert_snapshot!(lex_source(&source)); + } + + #[test] + fn backslash_continuation_mismatch_indentation() { + // Indentation doesn't match any previous indentation level + let source = r"if True: + 1 + \ + 2 +" + .to_string(); + assert_snapshot!(lex_invalid(&source, Mode::Module)); + } } diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap new file mode 100644 index 0000000000000..4df1ec54926c3 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_at_root.snap @@ -0,0 +1,95 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + ( + If, + 0..2, + ), + ( + True, + 3..7, + ), + ( + Colon, + 7..8, + ), + ( + Newline, + 8..9, + ), + ( + Indent, + 9..15, + ), + ( + Int( + 1, + ), + 15..16, + ), + ( + Newline, + 16..17, + ), + ( + If, + 21..23, + ), + ( + True, + 24..28, + ), + ( + Colon, + 28..29, + ), + ( + Newline, + 29..30, + ), + ( + Indent, + 30..40, + ), + ( + Int( + 2, + ), + 40..41, + ), + ( + Newline, + 41..42, + ), + ( + Dedent, + 42..42, + ), + ( + Dedent, + 42..42, + ), + ( + Else, + 42..46, + ), + ( + Colon, + 46..47, + ), + ( + Int( + 3, + ), + 53..54, + ), + ( + Newline, + 54..55, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap new file mode 100644 index 0000000000000..6639f9cbac551 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_indentation.snap @@ -0,0 +1,91 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + ( + If, + 0..2, + ), + ( + True, + 3..7, + ), + ( + Colon, + 7..8, + ), + ( + Newline, + 8..9, + ), + ( + Indent, + 9..13, + ), + ( + Int( + 1, + ), + 13..14, + ), + ( + Newline, + 14..15, + ), + ( + Int( + 2, + ), + 29..30, + ), + ( + Newline, + 30..31, + ), + ( + Int( + 3, + ), + 37..38, + ), + ( + Newline, + 38..39, + ), + ( + Dedent, + 39..39, + ), + ( + Else, + 39..43, + ), + ( + Colon, + 43..44, + ), + ( + Newline, + 44..45, + ), + ( + Indent, + 45..49, + ), + ( + Pass, + 49..53, + ), + ( + Newline, + 53..54, + ), + ( + Dedent, + 54..54, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap new file mode 100644 index 0000000000000..8ffbbe16f55e6 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__backslash_continuation_mismatch_indentation.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(&source, Mode::Module)" +--- +## Tokens +``` +[ + ( + If, + 0..2, + ), + ( + True, + 3..7, + ), + ( + Colon, + 7..8, + ), + ( + Newline, + 8..9, + ), + ( + Indent, + 9..13, + ), + ( + Int( + 1, + ), + 13..14, + ), + ( + Newline, + 14..15, + ), + ( + Unknown, + 15..23, + ), + ( + Int( + 2, + ), + 23..24, + ), + ( + Newline, + 24..25, + ), +] +``` +## Errors +``` +[ + LexicalError { + error: IndentationError, + location: 15..23, + }, +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap new file mode 100644 index 0000000000000..edcef8da028b6 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__multiple_backslash_continuation.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(&source) +--- +## Tokens +``` +[ + ( + If, + 0..2, + ), + ( + True, + 3..7, + ), + ( + Colon, + 7..8, + ), + ( + Newline, + 8..9, + ), + ( + Indent, + 9..13, + ), + ( + Int( + 1, + ), + 13..14, + ), + ( + Newline, + 14..15, + ), + ( + Int( + 2, + ), + 55..56, + ), + ( + Newline, + 56..57, + ), + ( + Dedent, + 57..57, + ), +] +``` diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap new file mode 100644 index 0000000000000..5694d581cc708 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap @@ -0,0 +1,77 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..29, + body: [ + If( + StmtIf { + node_index: NodeIndex(None), + range: 0..28, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 3..7, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 13..14, + value: NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 13..14, + value: Int( + 1, + ), + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 27..28, + value: NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 27..28, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + ], + }, +) +``` +## Errors + + | +1 | if True: +2 | 1 +3 | / \ +4 | | 2 + | |____^ Syntax Error: Unexpected indentation + | + + + | +3 | \ +4 | 2 + | ^ Syntax Error: Expected a statement + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@backslash_continuation_indentation.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@backslash_continuation_indentation.py.snap new file mode 100644 index 0000000000000..08b0a93d074fc --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@backslash_continuation_indentation.py.snap @@ -0,0 +1,84 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..46, + body: [ + If( + StmtIf { + node_index: NodeIndex(None), + range: 0..45, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 3..7, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 23..24, + value: NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 23..24, + value: Int( + 1, + ), + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 31..32, + value: NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 31..32, + value: Int( + 2, + ), + }, + ), + }, + ), + ], + elif_else_clauses: [ + ElifElseClause { + range: 33..45, + node_index: NodeIndex(None), + test: None, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..45, + value: NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 44..45, + value: Int( + 3, + ), + }, + ), + }, + ), + ], + }, + ], + }, + ), + ], + }, +) +``` From 1f380c82584a6dab7e8715bc7dd5ae187da1e69a Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 19 Feb 2026 14:29:35 +0100 Subject: [PATCH 003/261] [ty] Update tests `reveal_type` and `Never` (#23418) ## Summary `reveal_type(something_of_type_never)` is a call to a function that returns `Never`, since `reveal_type` returns the type of its argument. So it is conceivable that we treat it just like any other call to a terminal function in terms of control flow analysis. This change to our tests prepares us for that scenario in that it splits tests into multiple functions, so that assertions following a `reveal_type(something_of_type_never)` call can no longer be considered unreachable. --- .../resources/mdtest/annotations/never.md | 40 +++++---- .../resources/mdtest/attributes.md | 7 +- .../mdtest/exhaustiveness_checking.md | 1 - .../resources/mdtest/intersection_types.md | 85 +++++++++++-------- .../resources/mdtest/narrow/match.md | 2 + 5 files changed, 77 insertions(+), 58 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/never.md b/crates/ty_python_semantic/resources/mdtest/annotations/never.md index 62f0968d29977..688857f80e3ff 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/never.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/never.md @@ -22,27 +22,31 @@ reveal_type(stop()) from typing_extensions import NoReturn, Never, Any # error: [invalid-type-form] "Type `typing.Never` expected no type parameter" -x: Never[int] -a1: NoReturn -a2: Never -b1: Any -b2: int +invalid: Never[int] -def f(): +def _(never: Never): # revealed: Never - reveal_type(a1) + reveal_type(never) + +def _(noreturn: NoReturn): # revealed: Never - reveal_type(a2) - - # Never is assignable to all types. - v1: int = a1 - v2: str = a1 - # Other types are not assignable to Never except for Never (and Any). - v3: Never = b1 - v4: Never = a2 - v5: Any = b2 - # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Never`" - v6: Never = 1 + reveal_type(noreturn) + +# Never is assignable to all types: +def _(never: Never): + v1: int = never + v2: str = never + v3: Never = never + v4: Any = never + +# No type is assignable to Never except for Never (and Any): +def _(never: Never, noreturn: NoReturn, any: Any): + v1: Never = 1 # error: [invalid-assignment] + v2: Never = "a" # error: [invalid-assignment] + + v3: Never = any + v4: Never = noreturn + v4: NoReturn = never ``` ## `typing.Never` diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 4c734ff982d02..19f1c218836c3 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2125,11 +2125,12 @@ of type `Never`): ```py from typing_extensions import Never, Any -def _(n: Never): - reveal_type(n.__setattr__) # revealed: Never +def _(never: Never): + reveal_type(never.__setattr__) # revealed: Never +def _(never: Never): # No error: - n.non_existing = 1 + never.non_existing = 1 ``` And similarly for `Any`: diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 1d7cbfd401341..4b7137ef98b76 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -471,7 +471,6 @@ def i[T: (int, str)](x: T) -> T: case str(): pass case _: - reveal_type(x) # revealed: Never assert_never(x) return x diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md index 12c5c68f4a0a7..dd63a7ccc697d 100644 --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -313,24 +313,29 @@ class P: ... class Q: ... class R(Generic[T_co]): ... -def _( - i1: Intersection[P, Not[P]], - i2: Intersection[Not[P], P], - i3: Intersection[P, Q, Not[P]], - i4: Intersection[Not[P], Q, P], - i5: Intersection[P, Any, Not[P]], - i6: Intersection[Not[P], Any, P], - i7: Intersection[R[P], Not[R[P]]], - i8: Intersection[R[P], Not[R[Q]]], -) -> None: - reveal_type(i1) # revealed: Never - reveal_type(i2) # revealed: Never - reveal_type(i3) # revealed: Never - reveal_type(i4) # revealed: Never - reveal_type(i5) # revealed: Never - reveal_type(i6) # revealed: Never - reveal_type(i7) # revealed: Never - reveal_type(i8) # revealed: R[P] & ~R[Q] +def _(i: Intersection[P, Not[P]]) -> None: + reveal_type(i) # revealed: Never + +def _(i: Intersection[Not[P], P]) -> None: + reveal_type(i) # revealed: Never + +def _(i: Intersection[P, Q, Not[P]]) -> None: + reveal_type(i) # revealed: Never + +def _(i: Intersection[Not[P], Q, P]) -> None: + reveal_type(i) # revealed: Never + +def _(i: Intersection[P, Any, Not[P]]) -> None: + reveal_type(i) # revealed: Never + +def _(i: Intersection[Not[P], Any, P]) -> None: + reveal_type(i) # revealed: Never + +def _(i: Intersection[R[P], Not[R[P]]]) -> None: + reveal_type(i) # revealed: Never + +def _(i: Intersection[R[P], Not[R[Q]]]) -> None: + reveal_type(i) # revealed: R[P] & ~R[Q] ``` ### Union of a type and its negation @@ -784,41 +789,49 @@ type Red = Literal[Color.RED] type Green = Literal[Color.GREEN] type Blue = Literal[Color.BLUE] -def f( - a: Intersection[Color, Red], - b: Intersection[Color, Not[Red]], - c: Intersection[Color, Not[Red | Green]], - d: Intersection[Color, Not[Red | Green | Blue]], - e: Intersection[Red, Not[Color]], - f: Intersection[Red | Green, Not[Color]], - g: Intersection[Not[Red], Color], - h: Intersection[Red, Green], - i: Intersection[Red | Green, Green | Blue], -): +def _(a: Intersection[Color, Red]) -> None: reveal_type(a) # revealed: Literal[Color.RED] + +def _(b: Intersection[Color, Not[Red]]) -> None: reveal_type(b) # revealed: Literal[Color.GREEN, Color.BLUE] + +def _(c: Intersection[Color, Not[Red | Green]]) -> None: reveal_type(c) # revealed: Literal[Color.BLUE] + +def _(d: Intersection[Color, Not[Red | Green | Blue]]) -> None: reveal_type(d) # revealed: Never + +def _(e: Intersection[Red, Not[Color]]) -> None: reveal_type(e) # revealed: Never + +def _(f: Intersection[Red | Green, Not[Color]]) -> None: reveal_type(f) # revealed: Never + +def _(g: Intersection[Not[Red], Color]) -> None: reveal_type(g) # revealed: Literal[Color.GREEN, Color.BLUE] + +def _(h: Intersection[Red, Green]) -> None: reveal_type(h) # revealed: Never + +def _(i: Intersection[Red | Green, Green | Blue]) -> None: reveal_type(i) # revealed: Literal[Color.GREEN] class Single(Enum): VALUE = 0 -def g( - a: Intersection[Single, Literal[Single.VALUE]], - b: Intersection[Single, Not[Literal[Single.VALUE]]], - c: Intersection[Not[Literal[Single.VALUE]], Single], - d: Intersection[Single, Not[Single]], - e: Intersection[Single | int, Not[Single]], -): +def _(a: Intersection[Single, Literal[Single.VALUE]]) -> None: reveal_type(a) # revealed: Single + +def _(b: Intersection[Single, Not[Literal[Single.VALUE]]]) -> None: reveal_type(b) # revealed: Never + +def _(c: Intersection[Not[Literal[Single.VALUE]], Single]) -> None: reveal_type(c) # revealed: Never + +def _(d: Intersection[Single, Not[Single]]) -> None: reveal_type(d) # revealed: Never + +def _(e: Intersection[Single | int, Not[Single]]) -> None: reveal_type(e) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 2e1ad06abd8e3..d18d48b2b745c 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -228,12 +228,14 @@ def _(x: A | B | C): case _: reveal_type(x) # revealed: Never +def _(x: A | B | C): match x: case A() | B() | C(): reveal_type(x) # revealed: A | B | C case _: reveal_type(x) # revealed: Never +def _(x: A | B | C): match x: case A(): reveal_type(x) # revealed: A From 97acaaea5f993f33d3f5bb27c5db760a2f3d1e8a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 19 Feb 2026 11:03:15 -0500 Subject: [PATCH 004/261] [ty] Fix stack overflow for self-referential `TypeOf` in annotations (#23407) ## Summary When a function references itself via `TypeOf` in a deferred annotation (`def foo(x: "TypeOf[foo]")`), the resulting `FunctionType` contains itself as a parameter type, forming a cycle. Closes https://github.com/astral-sh/ty/issues/2800. --- .../resources/mdtest/ty_extensions.md | 12 +++++++ crates/ty_python_semantic/src/types.rs | 34 +++++++++++-------- .../ty_python_semantic/src/types/display.rs | 28 +++++++++++---- 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 09315697dac76..cd0a4fa342bd6 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -427,6 +427,18 @@ def f(x: TypeOf) -> None: reveal_type(x) # revealed: Unknown ``` +## Self-referential `TypeOf` in annotations + +A function can reference itself via `TypeOf` in a deferred annotation. This should not cause a stack +overflow: + +```py +from ty_extensions import TypeOf + +def foo(x: "TypeOf[foo]"): + reveal_type(x) # revealed: def foo(x: def foo(...)) -> Unknown +``` + ## `CallableTypeOf` The `CallableTypeOf` special form can be used to extract the `Callable` structural type inhabited by diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9fea5bf06d481..a4d17e190a163 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6408,25 +6408,27 @@ impl<'db> Type<'db> { }, } - Type::FunctionLiteral(function) => match type_mapping { - // Promote the types within the signature before promoting the signature to its - // callable form. - TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => { - Type::FunctionLiteral(function.apply_type_mapping_impl( + Type::FunctionLiteral(function) => visitor.visit(self, || { + match type_mapping { + // Promote the types within the signature before promoting the signature to its + // callable form. + TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => { + Type::FunctionLiteral(function.apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + )) + .promote_literals_impl(db) + } + _ => Type::FunctionLiteral(function.apply_type_mapping_impl( db, type_mapping, tcx, visitor, - )) - .promote_literals_impl(db) + )), } - _ => Type::FunctionLiteral(function.apply_type_mapping_impl( - db, - type_mapping, - tcx, - visitor, - )), - }, + }), Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( db, @@ -6692,7 +6694,9 @@ impl<'db> Type<'db> { } Type::FunctionLiteral(function) => { - function.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + visitor.visit(self, || { + function.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + }); } Type::BoundMethod(method) => { diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 3a23d6eab45f6..6428051d4bf5c 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -87,6 +87,9 @@ pub struct DisplaySettings<'db> { /// whose type parameters are currently being displayed). /// Used to suppress redundant `@{scope}` suffixes for type variables. pub active_scopes: Rc>>, + /// Function types that are currently being displayed. + /// Used to prevent infinite recursion when displaying self-referential function types. + pub visited_function_types: Rc>>, } impl<'db> DisplaySettings<'db> { @@ -1466,6 +1469,19 @@ pub(crate) struct DisplayFunctionType<'db> { impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + // Detect self-referential function types to prevent infinite recursion. + if self.settings.visited_function_types.contains(&self.ty) { + f.set_invalid_type_annotation(); + f.write_str("def ")?; + write!(f, "{}", self.ty.name(self.db))?; + return f.write_str("(...)"); + } + + let mut settings = self.settings.clone(); + let mut visited = (*settings.visited_function_types).clone(); + visited.insert(self.ty); + settings.visited_function_types = Rc::new(visited); + let signature = self.ty.signature(self.db); match signature.overloads.as_slice() { @@ -1475,7 +1491,7 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, - settings: self.settings.clone(), + settings: settings.clone(), hide_unused_self, }; f.set_invalid_type_annotation(); @@ -1483,24 +1499,24 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { write!(f, "{}", self.ty.name(self.db))?; type_parameters.fmt_detailed(f)?; signature - .display_with(self.db, self.settings.disallow_signature_name()) + .display_with(self.db, settings.disallow_signature_name()) .fmt_detailed(f) } signatures => { // TODO: How to display overloads? - if !self.settings.multiline { + if !settings.multiline { // TODO: This should ideally have a TypeDetail but we actually // don't have a type for @overload (we just detect the decorator) f.write_str("Overload")?; f.write_char('[')?; } - let separator = if self.settings.multiline { "\n" } else { ", " }; + let separator = if settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); for signature in signatures { - join.entry(&signature.display_with(self.db, self.settings.clone())); + join.entry(&signature.display_with(self.db, settings.clone())); } join.finish()?; - if !self.settings.multiline { + if !settings.multiline { f.write_str("]")?; } Ok(()) From 1425c185b0a47be87112762f65b5bf7e323fb950 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 17 Feb 2026 09:44:08 -0500 Subject: [PATCH 005/261] [ty] Add code folding support This PR implements the `textDocument/foldingRange` LSP request, enabling code folding in editors. We also support tagging each folding range with its "kind." So for example, this enables one to ask your editor to "collapse all block comments." The implementation works by doing a simple AST traversal to identify "blocks" in a Python program. We also do a line oriented search to extract ranges that are more difficult to do from the AST: blocks of comments, blocks of imports and special custom "regions." Closes astral-sh/ty#2588 --- crates/ruff_text_size/src/range.rs | 46 + crates/ty_ide/src/folding_range.rs | 1722 +++++++++++++++++ crates/ty_ide/src/lib.rs | 2 + crates/ty_server/src/capabilities.rs | 1 + crates/ty_server/src/server/api.rs | 3 + crates/ty_server/src/server/api/requests.rs | 2 + .../src/server/api/requests/folding_range.rs | 75 + crates/ty_server/tests/e2e/folding_range.rs | 32 + crates/ty_server/tests/e2e/main.rs | 25 +- ...ge__folding_range_basic_functionality.snap | 24 + .../e2e__initialize__initialization.snap | 1 + ...ialize__initialization_with_workspace.snap | 1 + 12 files changed, 1926 insertions(+), 8 deletions(-) create mode 100644 crates/ty_ide/src/folding_range.rs create mode 100644 crates/ty_server/src/server/api/requests/folding_range.rs create mode 100644 crates/ty_server/tests/e2e/folding_range.rs create mode 100644 crates/ty_server/tests/e2e/snapshots/e2e__folding_range__folding_range_basic_functionality.snap diff --git a/crates/ruff_text_size/src/range.rs b/crates/ruff_text_size/src/range.rs index 302568e8e3465..3d2fa9b33d0c9 100644 --- a/crates/ruff_text_size/src/range.rs +++ b/crates/ruff_text_size/src/range.rs @@ -345,6 +345,52 @@ impl TextRange { } } + /// Returns a new range with the start offset set to the + /// value given and the end offset unchanged from this + /// range. + /// + /// ## Panics + /// + /// When `offset > self.end()`. + /// + /// ## Examples + /// + /// ``` + /// use ruff_text_size::{Ranged, TextRange, TextSize}; + /// + /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); + /// let new = range.with_start(TextSize::from(8)); + /// assert_eq!(new, TextRange::new(TextSize::from(8), TextSize::from(10))); + /// ``` + #[inline] + #[must_use] + pub fn with_start(&self, offset: TextSize) -> TextRange { + TextRange::new(offset, self.end()) + } + + /// Returns a new range with the end offset set to the + /// value given and the start offset unchanged from this + /// range. + /// + /// ## Panics + /// + /// When `offset < self.start()`. + /// + /// ## Examples + /// + /// ``` + /// use ruff_text_size::{Ranged, TextRange, TextSize}; + /// + /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); + /// let new = range.with_end(TextSize::from(8)); + /// assert_eq!(new, TextRange::new(TextSize::from(5), TextSize::from(8))); + /// ``` + #[inline] + #[must_use] + pub fn with_end(&self, offset: TextSize) -> TextRange { + TextRange::new(self.start(), offset) + } + /// Subtracts an offset from the start position. /// /// diff --git a/crates/ty_ide/src/folding_range.rs b/crates/ty_ide/src/folding_range.rs new file mode 100644 index 0000000000000..aeb7e9208f007 --- /dev/null +++ b/crates/ty_ide/src/folding_range.rs @@ -0,0 +1,1722 @@ +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_db::source::source_text; +use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_body}; +use ruff_python_ast::{AnyNodeRef, Stmt}; +use ruff_source_file::{Line, UniversalNewlines}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; + +use crate::Db; + +/// The kind of a folding range. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FoldingRangeKind { + /// A comment block. + Comment, + /// An import block. + Imports, + /// A region (e.g., `# region` / `# endregion`). + Region, +} + +/// A folding range in the source code. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FoldingRange { + /// The range to fold. + pub range: TextRange, + /// The kind of folding range. + pub kind: Option, +} + +impl FoldingRange { + fn with_kind(self, kind: FoldingRangeKind) -> Self { + Self { + kind: Some(kind), + ..self + } + } +} + +impl From for FoldingRange { + fn from(range: TextRange) -> FoldingRange { + FoldingRange { range, kind: None } + } +} + +/// Returns a list of folding ranges for the given file. +pub fn folding_ranges(db: &dyn Db, file: File) -> Vec { + let parsed = parsed_module(db, file).load(db); + let source = source_text(db, file); + + let mut visitor = FoldingRangeVisitor { + source: source.as_str(), + ranges: vec![], + }; + visitor.visit_body(parsed.suite()); + + // Add docstring for module-level (first statement if it's a string literal). + visitor.add_docstring_range(parsed.suite()); + + // Add remaining ranges not covered by the AST visitor. + visitor.add_comment_ranges(); + visitor.add_custom_region_ranges(); + + visitor.ranges +} + +struct FoldingRangeVisitor<'a> { + source: &'a str, + ranges: Vec, +} + +impl<'a> FoldingRangeVisitor<'a> { + /// Add the given folding range if it spans multiple lines. + fn add_range(&mut self, folding_range: impl Into) { + let folding_range = folding_range.into(); + if !self.is_multiline(folding_range.range) { + return; + } + self.force_add_range(folding_range); + } + + /// Always adds the given range. + /// + /// This is useful when you always want a folding range even if + /// the range may not span multiple lines. For example, `else` + /// or `finally` blocks. + fn force_add_range(&mut self, folding_range: impl Into) { + let folding_range = folding_range.into(); + self.ranges.push(folding_range); + } + + /// Iterate over lines with their starting byte offsets. + fn lines(&self) -> impl Iterator> + use<'a> { + self.source.universal_newlines() + } + + fn is_multiline(&self, range: TextRange) -> bool { + self.source[range].contains('\n') || self.source[range].contains('\r') + } + + /// Compute folding ranges for consecutive import statements. + /// Import blocks separated by blank lines are folded separately. + /// + /// TODO: It might be better to move this logic into the AST + /// visitor via `enter_node`. I found it clearer to write it as + /// a single separate pass over a sequence of statements. But if + /// this ends up being a perf issue, it should be possible to + /// do this within the existing AST pass. + fn add_import_ranges(&mut self, stmts: &[Stmt]) { + let mut import_range: Option = None; + let mut prev_import_end: Option = None; + + for stmt in stmts { + if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) { + // Check if there's a blank line between this import and the previous one. + let has_blank_line = prev_import_end + .is_some_and(|prev_end| self.has_blank_line_between(prev_end, stmt.start())); + + if has_blank_line { + // Finalize the current import block and start a new one. + if let Some(range) = import_range { + self.add_range( + FoldingRange::from(range).with_kind(FoldingRangeKind::Imports), + ); + } + import_range = Some(stmt.range()); + } else if let Some(ref mut range) = import_range { + *range = range.with_end(stmt.end()); + } else { + import_range = Some(stmt.range()); + } + prev_import_end = Some(stmt.end()); + } else { + if let Some(range) = import_range { + self.add_range(FoldingRange::from(range).with_kind(FoldingRangeKind::Imports)); + } + import_range = None; + prev_import_end = None; + } + } + if let Some(range) = import_range { + self.add_range(FoldingRange::from(range).with_kind(FoldingRangeKind::Imports)); + } + } + + /// Check if there's a blank line appearing anywhere between two positions. + fn has_blank_line_between(&self, start: TextSize, end: TextSize) -> bool { + let mut count = 0; + for line in self.source[TextRange::new(start, end)].universal_newlines() { + if !line.is_empty() { + return count >= 2; + } + count += 1; + } + count >= 2 + } + + /// Compute folding ranges for `# region` / `# endregion` comments. + fn add_custom_region_ranges(&mut self) { + let mut region_starts: Vec = Vec::new(); + + for line in self.lines() { + let trimmed = line.trim_start(); + if trimmed.starts_with("# region") || trimmed.starts_with("#region") { + region_starts.push(line.start()); + } else if trimmed.starts_with("# endregion") || trimmed.starts_with("#endregion") { + if let Some(start) = region_starts.pop() { + let end = line.start() + line.trim_end().text_len(); + self.add_range( + FoldingRange::from(TextRange::new(start, end)) + .with_kind(FoldingRangeKind::Region), + ); + } + } + } + } + + /// Compute folding ranges for consecutive comment lines. + fn add_comment_ranges(&mut self) { + let mut comment_range: Option = None; + + for line in self.lines() { + let trimmed = line.trim_start(); + + // Check if this is a comment line (but not a region marker) + let is_comment = trimmed.starts_with('#') + && !trimmed.starts_with("# region") + && !trimmed.starts_with("#region") + && !trimmed.starts_with("# endregion") + && !trimmed.starts_with("#endregion"); + + if is_comment { + let end = line.start() + line.trim_end().text_len(); + if let Some(ref mut range) = comment_range { + *range = range.with_end(end); + } else { + comment_range = Some(TextRange::new(line.start(), end)); + } + } else if let Some(range) = comment_range { + self.add_range(FoldingRange::from(range).with_kind(FoldingRangeKind::Comment)); + comment_range = None; + } + } + if let Some(range) = comment_range { + self.add_range(FoldingRange::from(range).with_kind(FoldingRangeKind::Comment)); + } + } + + /// Add a folding range for a docstring if present at the start of a body. + /// Handles string literals, f-strings, and t-strings (but not bytes literals). + fn add_docstring_range(&mut self, body: &[Stmt]) { + let Some(first_stmt) = body.first() else { + return; + }; + let Stmt::Expr(ref expr_stmt) = *first_stmt else { + return; + }; + let is_string_like = expr_stmt.value.is_string_literal_expr() + || expr_stmt.value.is_f_string_expr() + || expr_stmt.value.is_t_string_expr(); + if !is_string_like { + return; + } + self.add_range(FoldingRange::from(first_stmt.range()).with_kind(FoldingRangeKind::Comment)); + } +} + +impl SourceOrderVisitor<'_> for FoldingRangeVisitor<'_> { + fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal { + match node { + // Compound statements that create folding regions + AnyNodeRef::StmtFunctionDef(func) => { + self.add_range(func.range()); + // Note that this may be duplicative with folding + // ranges added for string literals. But I don't think + // the LSP protocol specifies that this is a problem. + // If we do need to de-dupe, then we'll want to keep + // this one since it attaches a "comment" folding range + // kind to the range. So we'll need to skip over the + // corresponding range for the literal. + self.add_docstring_range(&func.body); + } + AnyNodeRef::StmtClassDef(class) => { + self.add_range(class.range()); + // See comment above for class docstrings about this + // being duplicative with adding folding ranges for + // string literals. + self.add_docstring_range(&class.body); + } + AnyNodeRef::StmtIf(if_stmt) => { + // Fold each branch individually rather than the entire if block. + // The if clause range is from the start of the if to the end of its body. + if let Some(last_stmt) = if_stmt.body.last() { + self.add_range(TextRange::new(if_stmt.start(), last_stmt.end())); + } + // Each elif/else clause has its own range. + for clause in &if_stmt.elif_else_clauses { + self.add_range(clause.range()); + } + } + AnyNodeRef::StmtFor(for_stmt) => { + // Fold the for body separately from the else block. + if let Some(last_stmt) = for_stmt.body.last() { + self.add_range(TextRange::new(for_stmt.start(), last_stmt.end())); + } + if let (Some(first), Some(last)) = (for_stmt.orelse.first(), for_stmt.orelse.last()) + { + self.add_range(TextRange::new(first.start(), last.end())); + } + } + AnyNodeRef::StmtWhile(while_stmt) => { + // Fold the while body separately from the else block. + if let Some(last_stmt) = while_stmt.body.last() { + self.add_range(TextRange::new(while_stmt.start(), last_stmt.end())); + } + if let (Some(first), Some(last)) = + (while_stmt.orelse.first(), while_stmt.orelse.last()) + { + self.add_range(TextRange::new(first.start(), last.end())); + } + } + AnyNodeRef::StmtWith(with_stmt) => { + self.add_range(with_stmt.range()); + } + AnyNodeRef::StmtTry(try_stmt) => { + // Fold the try body separately from handlers, else, and finally. + if let Some(last_stmt) = try_stmt.body.last() { + self.add_range(TextRange::new(try_stmt.start(), last_stmt.end())); + } + // Exception handlers are folded via ExceptHandlerExceptHandler. + // Fold the else block if present. + if let (Some(first), Some(last)) = (try_stmt.orelse.first(), try_stmt.orelse.last()) + { + self.force_add_range(TextRange::new(first.start(), last.end())); + } + // Fold the finally block if present. + if let (Some(first), Some(last)) = + (try_stmt.finalbody.first(), try_stmt.finalbody.last()) + { + self.force_add_range(TextRange::new(first.start(), last.end())); + } + } + AnyNodeRef::StmtMatch(match_stmt) => { + self.add_range(match_stmt.range()); + } + + // Match cases within match statements + AnyNodeRef::MatchCase(case) => { + self.add_range(case.range()); + } + + // Exception handlers + AnyNodeRef::ExceptHandlerExceptHandler(handler) => { + self.add_range(handler.range()); + } + + // Multiline expressions + AnyNodeRef::ExprList(list) => { + self.add_range(list.range()); + } + AnyNodeRef::ExprTuple(tuple) => { + // Only fold parenthesized tuples. + if tuple.parenthesized { + self.add_range(tuple.range()); + } + } + AnyNodeRef::ExprDict(dict) => { + self.add_range(dict.range()); + } + AnyNodeRef::ExprSet(set) => { + self.add_range(set.range()); + } + AnyNodeRef::ExprListComp(listcomp) => { + self.add_range(listcomp.range()); + } + AnyNodeRef::ExprSetComp(setcomp) => { + self.add_range(setcomp.range()); + } + AnyNodeRef::ExprDictComp(dictcomp) => { + self.add_range(dictcomp.range()); + } + AnyNodeRef::ExprGenerator(generator) => { + self.add_range(generator.range()); + } + + // Function calls with arguments spanning multiple lines + AnyNodeRef::ExprCall(call) => { + self.add_range(call.range()); + } + + // String and bytes literals + AnyNodeRef::ExprStringLiteral(string) => { + self.add_range(string.range()); + } + AnyNodeRef::ExprBytesLiteral(bytes) => { + self.add_range(bytes.range()); + } + AnyNodeRef::ExprFString(fstring) => { + self.add_range(fstring.range()); + } + AnyNodeRef::ExprTString(tstring) => { + self.add_range(tstring.range()); + } + + // Type parameter lists + AnyNodeRef::TypeParams(params) => { + self.add_range(params.range()); + } + + _ => {} + } + + TraversalSignal::Traverse + } + + fn visit_body(&mut self, body: &'_ [Stmt]) { + // Handle import blocks in any body (module, function, class, etc.). + self.add_import_ranges(body); + walk_body(self, body); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::CursorTest; + use insta::assert_snapshot; + use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span}; + + #[test] + fn test_folding_range_class() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +class MyClass: + def __init__(self): + self.value = 1 + + def method(self): + return self.value + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / class MyClass: + 3 | | def __init__(self): + 4 | | self.value = 1 + 5 | | + 6 | | def method(self): + 7 | | return self.value + | |_________________________^ + | + + info[folding-range]: Folding Range + --> main.py:3:5 + | + 2 | class MyClass: + 3 | / def __init__(self): + 4 | | self.value = 1 + | |______________________^ + 5 | + 6 | def method(self): + | + + info[folding-range]: Folding Range + --> main.py:6:5 + | + 4 | self.value = 1 + 5 | + 6 | / def method(self): + 7 | | return self.value + | |_________________________^ + | + "); + } + + #[test] + fn test_folding_range_attribute_comments() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +class MyClass: + def __init__(self): + self.value = 1 + """ + This is an + attribute comment. + """ + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r#" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / class MyClass: + 3 | | def __init__(self): + 4 | | self.value = 1 + 5 | | """ + 6 | | This is an + 7 | | attribute comment. + 8 | | """ + | |___________^ + | + + info[folding-range]: Folding Range + --> main.py:3:5 + | + 2 | class MyClass: + 3 | / def __init__(self): + 4 | | self.value = 1 + 5 | | """ + 6 | | This is an + 7 | | attribute comment. + 8 | | """ + | |___________^ + | + + info[folding-range]: Folding Range + --> main.py:5:9 + | + 3 | def __init__(self): + 4 | self.value = 1 + 5 | / """ + 6 | | This is an + 7 | | attribute comment. + 8 | | """ + | |___________^ + | + "#); + } + + #[test] + fn test_folding_range_imports_basic() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +import os +import sys +from typing import List, Dict + +def main(): + pass +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range (imports) + --> main.py:2:1 + | + 2 | / import os + 3 | | import sys + 4 | | from typing import List, Dict + | |_____________________________^ + 5 | + 6 | def main(): + | + + info[folding-range]: Folding Range + --> main.py:6:1 + | + 4 | from typing import List, Dict + 5 | + 6 | / def main(): + 7 | | pass + | |________^ + | + "); + } + + #[test] + fn test_folding_range_imports_blocks1() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +import os +import sys + +import numpy +import pandas +import requests + + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range (imports) + --> main.py:2:1 + | + 2 | / import os + 3 | | import sys + | |__________^ + 4 | + 5 | import numpy + | + + info[folding-range]: Folding Range (imports) + --> main.py:5:1 + | + 3 | import sys + 4 | + 5 | / import numpy + 6 | | import pandas + 7 | | import requests + | |_______________^ + | + "); + } + + #[test] + fn test_folding_range_imports_blocks2() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +import os +from math import prod + +try: + import foo + import bar +except ImportError: + first = None + bar = None + +import requests +from fastapi import FastAPI + + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range (imports) + --> main.py:2:1 + | + 2 | / import os + 3 | | from math import prod + | |_____________________^ + 4 | + 5 | try: + | + + info[folding-range]: Folding Range (imports) + --> main.py:12:1 + | + 10 | bar = None + 11 | + 12 | / import requests + 13 | | from fastapi import FastAPI + | |___________________________^ + | + + info[folding-range]: Folding Range + --> main.py:5:1 + | + 3 | from math import prod + 4 | + 5 | / try: + 6 | | import foo + 7 | | import bar + | |______________^ + 8 | except ImportError: + 9 | first = None + | + + info[folding-range]: Folding Range (imports) + --> main.py:6:5 + | + 5 | try: + 6 | / import foo + 7 | | import bar + | |______________^ + 8 | except ImportError: + 9 | first = None + | + + info[folding-range]: Folding Range + --> main.py:8:1 + | + 6 | import foo + 7 | import bar + 8 | / except ImportError: + 9 | | first = None + 10 | | bar = None + | |______________^ + 11 | + 12 | import requests + | + "); + } + + #[test] + fn test_folding_range_imports_nested() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +def my_function(): + import os + import sys + + import numpy + import pandas + + do_something() + + +class MyClass: + import typing + import collections + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / def my_function(): + 3 | | import os + 4 | | import sys + 5 | | + 6 | | import numpy + 7 | | import pandas + 8 | | + 9 | | do_something() + | |__________________^ + | + + info[folding-range]: Folding Range (imports) + --> main.py:3:5 + | + 2 | def my_function(): + 3 | / import os + 4 | | import sys + | |______________^ + 5 | + 6 | import numpy + | + + info[folding-range]: Folding Range (imports) + --> main.py:6:5 + | + 4 | import sys + 5 | + 6 | / import numpy + 7 | | import pandas + | |_________________^ + 8 | + 9 | do_something() + | + + info[folding-range]: Folding Range + --> main.py:12:1 + | + 12 | / class MyClass: + 13 | | import typing + 14 | | import collections + | |______________________^ + | + + info[folding-range]: Folding Range (imports) + --> main.py:13:5 + | + 12 | class MyClass: + 13 | / import typing + 14 | | import collections + | |______________________^ + | + "); + } + + #[test] + fn test_folding_range_control_flow() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +if condition: + do_something() +elif other: + do_other() +else: + default() + +for item in items: + process(item) +else: + okay() + +while running: + continue_work() +else: + doit() + + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / if condition: + 3 | | do_something() + | |__________________^ + 4 | elif other: + 5 | do_other() + | + + info[folding-range]: Folding Range + --> main.py:4:1 + | + 2 | if condition: + 3 | do_something() + 4 | / elif other: + 5 | | do_other() + | |______________^ + 6 | else: + 7 | default() + | + + info[folding-range]: Folding Range + --> main.py:6:1 + | + 4 | elif other: + 5 | do_other() + 6 | / else: + 7 | | default() + | |_____________^ + 8 | + 9 | for item in items: + | + + info[folding-range]: Folding Range + --> main.py:9:1 + | + 7 | default() + 8 | + 9 | / for item in items: + 10 | | process(item) + | |_________________^ + 11 | else: + 12 | okay() + | + + info[folding-range]: Folding Range + --> main.py:14:1 + | + 12 | okay() + 13 | + 14 | / while running: + 15 | | continue_work() + | |___________________^ + 16 | else: + 17 | doit() + | + "); + } + + #[test] + fn test_folding_range_nested_control_flow() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +if condition: + while running: + do_this() + and_that() + if maybe: + and_maybe_this() + and_maybe_this() + and_maybe_this() + and_maybe_this() + + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / if condition: + 3 | | while running: + 4 | | do_this() + 5 | | and_that() + 6 | | if maybe: + 7 | | and_maybe_this() + 8 | | and_maybe_this() + 9 | | and_maybe_this() + 10 | | and_maybe_this() + | |____________________________^ + | + + info[folding-range]: Folding Range + --> main.py:3:5 + | + 2 | if condition: + 3 | / while running: + 4 | | do_this() + 5 | | and_that() + 6 | | if maybe: + 7 | | and_maybe_this() + 8 | | and_maybe_this() + 9 | | and_maybe_this() + 10 | | and_maybe_this() + | |____________________________^ + | + + info[folding-range]: Folding Range + --> main.py:6:9 + | + 4 | do_this() + 5 | and_that() + 6 | / if maybe: + 7 | | and_maybe_this() + 8 | | and_maybe_this() + 9 | | and_maybe_this() + 10 | | and_maybe_this() + | |____________________________^ + | + "); + } + + #[test] + fn test_folding_range_loop_else() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +for item in items: + process(item) + validate(item) +else: + log_success() + notify_complete() + +while condition: + do_work() + check_status() +else: + handle_done() + cleanup_resources() + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / for item in items: + 3 | | process(item) + 4 | | validate(item) + | |__________________^ + 5 | else: + 6 | log_success() + | + + info[folding-range]: Folding Range + --> main.py:6:5 + | + 4 | validate(item) + 5 | else: + 6 | / log_success() + 7 | | notify_complete() + | |_____________________^ + 8 | + 9 | while condition: + | + + info[folding-range]: Folding Range + --> main.py:9:1 + | + 7 | notify_complete() + 8 | + 9 | / while condition: + 10 | | do_work() + 11 | | check_status() + | |__________________^ + 12 | else: + 13 | handle_done() + | + + info[folding-range]: Folding Range + --> main.py:13:5 + | + 11 | check_status() + 12 | else: + 13 | / handle_done() + 14 | | cleanup_resources() + | |_______________________^ + | + "); + } + + #[test] + fn test_folding_range_try_except() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +try: + risky_operation() +except ValueError: + handle_value_error() +except TypeError: + handle_type_error() +else: + success_action() +finally: + cleanup() + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / try: + 3 | | risky_operation() + | |_____________________^ + 4 | except ValueError: + 5 | handle_value_error() + | + + info[folding-range]: Folding Range + --> main.py:9:5 + | + 7 | handle_type_error() + 8 | else: + 9 | success_action() + | ^^^^^^^^^^^^^^^^ + 10 | finally: + 11 | cleanup() + | + + info[folding-range]: Folding Range + --> main.py:11:5 + | + 9 | success_action() + 10 | finally: + 11 | cleanup() + | ^^^^^^^^^ + | + + info[folding-range]: Folding Range + --> main.py:4:1 + | + 2 | try: + 3 | risky_operation() + 4 | / except ValueError: + 5 | | handle_value_error() + | |________________________^ + 6 | except TypeError: + 7 | handle_type_error() + | + + info[folding-range]: Folding Range + --> main.py:6:1 + | + 4 | except ValueError: + 5 | handle_value_error() + 6 | / except TypeError: + 7 | | handle_type_error() + | |_______________________^ + 8 | else: + 9 | success_action() + | + "); + } + + #[test] + fn test_folding_range_collections() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +my_list = [ + 1, + 2, + 3, +] + +my_dict = { + "a": 1, + "b": 2, +} + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r#" + info[folding-range]: Folding Range + --> main.py:2:11 + | + 2 | my_list = [ + | ___________^ + 3 | | 1, + 4 | | 2, + 5 | | 3, + 6 | | ] + | |_^ + 7 | + 8 | my_dict = { + | + + info[folding-range]: Folding Range + --> main.py:8:11 + | + 6 | ] + 7 | + 8 | my_dict = { + | ___________^ + 9 | | "a": 1, + 10 | | "b": 2, + 11 | | } + | |_^ + | + "#); + } + + #[test] + fn test_folding_range_string_literals() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +multiline_string = """ +This is a +multiline string +""" + +multiline_bytes = b""" +This is +multiline bytes +""" + +multiline_fstring = f""" +This is a +multiline f-string +""" + +multiline_tstring = t""" +This is a +multiline t-string +""" + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r#" + info[folding-range]: Folding Range + --> main.py:2:20 + | + 2 | multiline_string = """ + | ____________________^ + 3 | | This is a + 4 | | multiline string + 5 | | """ + | |___^ + 6 | + 7 | multiline_bytes = b""" + | + + info[folding-range]: Folding Range + --> main.py:7:19 + | + 5 | """ + 6 | + 7 | multiline_bytes = b""" + | ___________________^ + 8 | | This is + 9 | | multiline bytes + 10 | | """ + | |___^ + 11 | + 12 | multiline_fstring = f""" + | + + info[folding-range]: Folding Range + --> main.py:12:21 + | + 10 | """ + 11 | + 12 | multiline_fstring = f""" + | _____________________^ + 13 | | This is a + 14 | | multiline f-string + 15 | | """ + | |___^ + 16 | + 17 | multiline_tstring = t""" + | + + info[folding-range]: Folding Range + --> main.py:17:21 + | + 15 | """ + 16 | + 17 | multiline_tstring = t""" + | _____________________^ + 18 | | This is a + 19 | | multiline t-string + 20 | | """ + | |___^ + | + "#); + } + + #[test] + fn test_folding_range_match() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +match value: + case 1: + one() + case 2: + two() + case _: + default() + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / match value: + 3 | | case 1: + 4 | | one() + 5 | | case 2: + 6 | | two() + 7 | | case _: + 8 | | default() + | |_________________^ + | + + info[folding-range]: Folding Range + --> main.py:3:5 + | + 2 | match value: + 3 | / case 1: + 4 | | one() + | |_____________^ + 5 | case 2: + 6 | two() + | + + info[folding-range]: Folding Range + --> main.py:5:5 + | + 3 | case 1: + 4 | one() + 5 | / case 2: + 6 | | two() + | |_____________^ + 7 | case _: + 8 | default() + | + + info[folding-range]: Folding Range + --> main.py:7:5 + | + 5 | case 2: + 6 | two() + 7 | / case _: + 8 | | default() + | |_________________^ + | + "); + } + + #[test] + fn test_folding_range_regions() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +# region Imports +import os +import sys +# endregion + +# region Main +def main(): + pass +# endregion + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range (imports) + --> main.py:3:1 + | + 2 | # region Imports + 3 | / import os + 4 | | import sys + | |__________^ + 5 | # endregion + | + + info[folding-range]: Folding Range + --> main.py:8:1 + | + 7 | # region Main + 8 | / def main(): + 9 | | pass + | |________^ + 10 | # endregion + | + + info[folding-range]: Folding Range (region) + --> main.py:2:1 + | + 2 | / # region Imports + 3 | | import os + 4 | | import sys + 5 | | # endregion + | |___________^ + 6 | + 7 | # region Main + | + + info[folding-range]: Folding Range (region) + --> main.py:7:1 + | + 5 | # endregion + 6 | + 7 | / # region Main + 8 | | def main(): + 9 | | pass + 10 | | # endregion + | |___________^ + | + "); + } + + #[test] + fn test_folding_range_docstring() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +def my_function(): + """ + This is a multiline + docstring. + """ + pass + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r#" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / def my_function(): + 3 | | """ + 4 | | This is a multiline + 5 | | docstring. + 6 | | """ + 7 | | pass + | |________^ + | + + info[folding-range]: Folding Range (comment) + --> main.py:3:5 + | + 2 | def my_function(): + 3 | / """ + 4 | | This is a multiline + 5 | | docstring. + 6 | | """ + | |_______^ + 7 | pass + | + + info[folding-range]: Folding Range + --> main.py:3:5 + | + 2 | def my_function(): + 3 | / """ + 4 | | This is a multiline + 5 | | docstring. + 6 | | """ + | |_______^ + 7 | pass + | + "#); + } + + #[test] + fn test_folding_range_docstring_variants() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +def with_fstring_doc(): + f""" + This is an f-string + used as a docstring. + """ + pass + + +def with_tstring_doc(): + t""" + This is a t-string + used as a docstring. + """ + pass + + +def with_rawstring_doc(): + r""" + This is a raw string + used as a docstring. + """ + pass + + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r#" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / def with_fstring_doc(): + 3 | | f""" + 4 | | This is an f-string + 5 | | used as a docstring. + 6 | | """ + 7 | | pass + | |________^ + | + + info[folding-range]: Folding Range (comment) + --> main.py:3:5 + | + 2 | def with_fstring_doc(): + 3 | / f""" + 4 | | This is an f-string + 5 | | used as a docstring. + 6 | | """ + | |_______^ + 7 | pass + | + + info[folding-range]: Folding Range + --> main.py:3:5 + | + 2 | def with_fstring_doc(): + 3 | / f""" + 4 | | This is an f-string + 5 | | used as a docstring. + 6 | | """ + | |_______^ + 7 | pass + | + + info[folding-range]: Folding Range + --> main.py:10:1 + | + 10 | / def with_tstring_doc(): + 11 | | t""" + 12 | | This is a t-string + 13 | | used as a docstring. + 14 | | """ + 15 | | pass + | |________^ + | + + info[folding-range]: Folding Range (comment) + --> main.py:11:5 + | + 10 | def with_tstring_doc(): + 11 | / t""" + 12 | | This is a t-string + 13 | | used as a docstring. + 14 | | """ + | |_______^ + 15 | pass + | + + info[folding-range]: Folding Range + --> main.py:11:5 + | + 10 | def with_tstring_doc(): + 11 | / t""" + 12 | | This is a t-string + 13 | | used as a docstring. + 14 | | """ + | |_______^ + 15 | pass + | + + info[folding-range]: Folding Range + --> main.py:18:1 + | + 18 | / def with_rawstring_doc(): + 19 | | r""" + 20 | | This is a raw string + 21 | | used as a docstring. + 22 | | """ + 23 | | pass + | |________^ + | + + info[folding-range]: Folding Range (comment) + --> main.py:19:5 + | + 18 | def with_rawstring_doc(): + 19 | / r""" + 20 | | This is a raw string + 21 | | used as a docstring. + 22 | | """ + | |_______^ + 23 | pass + | + + info[folding-range]: Folding Range + --> main.py:19:5 + | + 18 | def with_rawstring_doc(): + 19 | / r""" + 20 | | This is a raw string + 21 | | used as a docstring. + 22 | | """ + | |_______^ + 23 | pass + | + "#); + } + + #[test] + fn test_folding_range_comments() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +# This is a comment block +# that spans multiple lines +# explaining something important + +def foo(): + pass + +# Another comment block +# with more details + +"#, + ) + .build(); + + assert_snapshot!( + test.folding_ranges(), + @r" + info[folding-range]: Folding Range + --> main.py:6:1 + | + 4 | # explaining something important + 5 | + 6 | / def foo(): + 7 | | pass + | |________^ + 8 | + 9 | # Another comment block + | + + info[folding-range]: Folding Range (comment) + --> main.py:2:1 + | + 2 | / # This is a comment block + 3 | | # that spans multiple lines + 4 | | # explaining something important + | |________________________________^ + 5 | + 6 | def foo(): + | + + info[folding-range]: Folding Range (comment) + --> main.py:9:1 + | + 7 | pass + 8 | + 9 | / # Another comment block + 10 | | # with more details + | |___________________^ + | + ", + ); + } + + #[test] + fn test_folding_range_with() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +with open("file.txt") as f: + content = f.read() + process(content) + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @r#" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / with open("file.txt") as f: + 3 | | content = f.read() + 4 | | process(content) + | |____________________^ + | + "#); + } + + #[test] + fn test_folding_multiline() { + // A class definition on a single line shouldn't have + // any folding ranges. + let test = CursorTest::builder() + .source("main.py", "class MyClass: pass\n") + .build(); + assert_snapshot!(test.folding_ranges(), @"No folding ranges found"); + + // A single LF new-line results in a folding range. + let test = CursorTest::builder() + .source("main.py", "class MyClass:\n pass\n") + .build(); + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:1:1 + | + 1 | / class MyClass: + 2 | | pass + | |________^ + | + "); + + // So does a single CRLF new-line. + let test = CursorTest::builder() + .source("main.py", "class MyClass:\r\n pass\r\n") + .build(); + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:1:1 + | + 1 | / class MyClass: + 2 | | pass + | |________^ + | + "); + + // And so to does a single CR new-line. + let test = CursorTest::builder() + .source("main.py", "class MyClass:\r pass\r") + .build(); + assert_snapshot!(test.folding_ranges(), @r" + info[folding-range]: Folding Range + --> main.py:1:1 + | + 1 | / class MyClass: + 2 | | pass + | |________^ + | + "); + } + + impl CursorTest { + fn folding_ranges(&self) -> String { + let ranges = folding_ranges(&self.db, self.cursor.file); + + if ranges.is_empty() { + return "No folding ranges found".to_string(); + } + + let diagnostics: Vec = ranges + .into_iter() + .map(|fr| FoldingRangeDiagnostic::new(self.cursor.file, fr)) + .collect(); + + self.render_diagnostics(diagnostics) + } + } + + struct FoldingRangeDiagnostic { + file: File, + folding_range: FoldingRange, + } + + impl FoldingRangeDiagnostic { + fn new(file: File, folding_range: FoldingRange) -> Self { + Self { + file, + folding_range, + } + } + } + + impl crate::tests::IntoDiagnostic for FoldingRangeDiagnostic { + fn into_diagnostic(self) -> Diagnostic { + let message = match self.folding_range.kind { + Some(FoldingRangeKind::Comment) => "Folding Range (comment)", + Some(FoldingRangeKind::Imports) => "Folding Range (imports)", + Some(FoldingRangeKind::Region) => "Folding Range (region)", + None => "Folding Range", + }; + + let mut diagnostic = Diagnostic::new( + DiagnosticId::Lint(LintName::of("folding-range")), + Severity::Info, + message.to_string(), + ); + + diagnostic.annotate(Annotation::primary( + Span::from(self.file).with_range(self.folding_range.range), + )); + + diagnostic + } + } +} diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index cec4aae5e9e31..bc095df78b75c 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -9,6 +9,7 @@ mod doc_highlights; mod docstring; mod document_symbols; mod find_references; +mod folding_range; mod goto; mod goto_declaration; mod goto_definition; @@ -32,6 +33,7 @@ pub use completion::{Completion, CompletionKind, CompletionSettings, completion} pub use doc_highlights::document_highlights; pub use document_symbols::document_symbols; pub use find_references::find_references; +pub use folding_range::{FoldingRange, FoldingRangeKind, folding_ranges}; pub use goto::{goto_declaration, goto_definition, goto_type_definition}; pub use hover::hover; pub use inlay_hints::{ diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index 0391efcc64826..21a3c13474ebf 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -447,6 +447,7 @@ pub(crate) fn server_capabilities( ..Default::default() }), selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), + folding_range_provider: Some(types::FoldingRangeProviderCapability::Simple(true)), document_symbol_provider: Some(OneOf::Left(true)), workspace_symbol_provider: Some(OneOf::Left(true)), notebook_document_sync: Some(OneOf::Left(lsp_types::NotebookDocumentSyncOptions { diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index 9b3401c8e34e1..0d35ff30cc406 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -97,6 +97,9 @@ pub(super) fn request(req: server::Request) -> Task { requests::SelectionRangeRequestHandler::METHOD => background_document_request_task::< requests::SelectionRangeRequestHandler, >(req, BackgroundSchedule::Worker), + requests::FoldingRangeRequestHandler::METHOD => background_document_request_task::< + requests::FoldingRangeRequestHandler, + >(req, BackgroundSchedule::Worker), requests::DocumentSymbolRequestHandler::METHOD => background_document_request_task::< requests::DocumentSymbolRequestHandler, >(req, BackgroundSchedule::Worker), diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs index 52d94f8bbfe51..8b1cafa2dd197 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -4,6 +4,7 @@ mod diagnostic; mod doc_highlights; mod document_symbols; mod execute_command; +mod folding_range; mod goto_declaration; mod goto_definition; mod goto_type_definition; @@ -26,6 +27,7 @@ pub(super) use diagnostic::DocumentDiagnosticRequestHandler; pub(super) use doc_highlights::DocumentHighlightRequestHandler; pub(super) use document_symbols::DocumentSymbolRequestHandler; pub(super) use execute_command::ExecuteCommand; +pub(super) use folding_range::FoldingRangeRequestHandler; pub(super) use goto_declaration::GotoDeclarationRequestHandler; pub(super) use goto_definition::GotoDefinitionRequestHandler; pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/folding_range.rs b/crates/ty_server/src/server/api/requests/folding_range.rs new file mode 100644 index 0000000000000..baa4397201440 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/folding_range.rs @@ -0,0 +1,75 @@ +use std::borrow::Cow; + +use lsp_types::request::FoldingRangeRequest; +use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams, Url}; +use ty_ide::folding_ranges; +use ty_project::ProjectDatabase; + +use crate::document::ToRangeExt; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::DocumentSnapshot; +use crate::session::client::Client; + +pub(crate) struct FoldingRangeRequestHandler; + +impl RequestHandler for FoldingRangeRequestHandler { + type RequestType = FoldingRangeRequest; +} + +impl BackgroundDocumentRequestHandler for FoldingRangeRequestHandler { + fn document_url(params: &FoldingRangeParams) -> Cow<'_, Url> { + Cow::Borrowed(¶ms.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: &DocumentSnapshot, + _client: &Client, + _params: FoldingRangeParams, + ) -> crate::server::Result>> { + if snapshot + .workspace_settings() + .is_language_services_disabled() + { + return Ok(None); + } + + let Some(file) = snapshot.to_notebook_or_file(db) else { + return Ok(None); + }; + + let results: Vec<_> = folding_ranges(db, file) + .into_iter() + .filter_map(|folding_range| { + let lsp_range = folding_range + .range + .to_lsp_range(db, file, snapshot.encoding())?; + + let kind = folding_range.kind.map(|k| match k { + ty_ide::FoldingRangeKind::Comment => FoldingRangeKind::Comment, + ty_ide::FoldingRangeKind::Imports => FoldingRangeKind::Imports, + ty_ide::FoldingRangeKind::Region => FoldingRangeKind::Region, + }); + + Some(FoldingRange { + start_line: lsp_range.local_range().start.line, + start_character: Some(lsp_range.local_range().start.character), + end_line: lsp_range.local_range().end.line, + end_character: Some(lsp_range.local_range().end.character), + kind, + collapsed_text: None, + }) + }) + .collect(); + + if results.is_empty() { + Ok(None) + } else { + Ok(Some(results)) + } + } +} + +impl RetriableRequestHandler for FoldingRangeRequestHandler {} diff --git a/crates/ty_server/tests/e2e/folding_range.rs b/crates/ty_server/tests/e2e/folding_range.rs new file mode 100644 index 0000000000000..cf5638c2b1ba9 --- /dev/null +++ b/crates/ty_server/tests/e2e/folding_range.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use ruff_db::system::SystemPath; + +use crate::TestServerBuilder; + +#[test] +fn folding_range_basic_functionality() -> Result<()> { + let workspace_root = SystemPath::new("src"); + let foo = SystemPath::new("src/foo.py"); + let foo_content = r#"class MyClass: + def __init__(self): + self.value = 1 + + def method(self): + return self.value +"#; + + let mut server = TestServerBuilder::new()? + .enable_pull_diagnostics(true) + .with_workspace(workspace_root, None)? + .with_file(foo, foo_content)? + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document(foo, foo_content, 1); + + let ranges = server.folding_range_request(&server.file_uri(foo)); + + insta::assert_json_snapshot!(ranges); + + Ok(()) +} diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index 5e4a98cbe0adc..aba9fa60f889e 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -31,6 +31,7 @@ mod code_actions; mod commands; mod completions; mod configuration; +mod folding_range; mod initialize; mod inlay_hints; mod notebook; @@ -67,14 +68,14 @@ use lsp_types::{ DidChangeTextDocumentParams, DidChangeWatchedFilesClientCapabilities, DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentDiagnosticParams, DocumentDiagnosticReportResult, FileEvent, - Hover, HoverParams, InitializeParams, InitializeResult, InitializedParams, InlayHint, - InlayHintClientCapabilities, InlayHintParams, NumberOrString, PartialResultParams, Position, - PreviousResultId, PublishDiagnosticsClientCapabilities, Range, SemanticTokensResult, - SignatureHelp, SignatureHelpParams, SignatureHelpTriggerKind, TextDocumentClientCapabilities, - TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem, - TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, WorkDoneProgressParams, - WorkspaceClientCapabilities, WorkspaceDiagnosticParams, WorkspaceDiagnosticReportResult, - WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersChangeEvent, + FoldingRange, FoldingRangeParams, Hover, HoverParams, InitializeParams, InitializeResult, + InitializedParams, InlayHint, InlayHintClientCapabilities, InlayHintParams, NumberOrString, + PartialResultParams, Position, PreviousResultId, PublishDiagnosticsClientCapabilities, Range, + SemanticTokensResult, SignatureHelp, SignatureHelpParams, SignatureHelpTriggerKind, + TextDocumentClientCapabilities, TextDocumentContentChangeEvent, TextDocumentIdentifier, + TextDocumentItem, TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, + WorkDoneProgressParams, WorkspaceClientCapabilities, WorkspaceDiagnosticParams, + WorkspaceDiagnosticReportResult, WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersChangeEvent, }; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf, TestSystem}; use rustc_hash::FxHashMap; @@ -1045,6 +1046,14 @@ impl TestServer { ) } + pub(crate) fn folding_range_request(&mut self, uri: &Url) -> Option> { + self.send_request_await::(FoldingRangeParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }) + } + /// Adds a workspace folder configuration to this wrapper's state. /// /// This is meant to roughly model VS Code's "Add Folder to Workspace" diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__folding_range__folding_range_basic_functionality.snap b/crates/ty_server/tests/e2e/snapshots/e2e__folding_range__folding_range_basic_functionality.snap new file mode 100644 index 0000000000000..88f8cec0ceb04 --- /dev/null +++ b/crates/ty_server/tests/e2e/snapshots/e2e__folding_range__folding_range_basic_functionality.snap @@ -0,0 +1,24 @@ +--- +source: crates/ty_server/tests/e2e/folding_range.rs +expression: ranges +--- +[ + { + "startLine": 0, + "startCharacter": 0, + "endLine": 5, + "endCharacter": 25 + }, + { + "startLine": 1, + "startCharacter": 4, + "endLine": 2, + "endCharacter": 22 + }, + { + "startLine": 4, + "startCharacter": 4, + "endLine": 5, + "endCharacter": 25 + } +] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap index 625262afc0ef3..63fba38b3c9cd 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap @@ -51,6 +51,7 @@ expression: initialization_result "renameProvider": { "prepareProvider": true }, + "foldingRangeProvider": true, "declarationProvider": true, "executeCommandProvider": { "commands": [ diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap index 625262afc0ef3..63fba38b3c9cd 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap @@ -51,6 +51,7 @@ expression: initialization_result "renameProvider": { "prepareProvider": true }, + "foldingRangeProvider": true, "declarationProvider": true, "executeCommandProvider": { "commands": [ From 729610acd9e19f57526e8ca40f355626154826bb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 19 Feb 2026 12:33:26 -0500 Subject: [PATCH 006/261] [ty] Fall back to ambiguous for large control flow graphs (#23399) ## Summary This [giant loop](https://github.com/Taiko2k/Tauon/blob/08bfd249f404817e58078a2cbce7a69d3f949f0c/src/tauon/t_modules/t_main.py#L44882-L49772) is causing us to create (per Claude) over 3 million nodes in the TDD graph. We now cap the analysis, which causes us to fall back to "ambiguous" -- so we can still detect most diagnostics, but lose some capabilities. Closes https://github.com/astral-sh/ty/issues/2846. --- .../reachability_constraints.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 8ae788833d3ed..2ddae26406dd4 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -314,6 +314,12 @@ const AMBIGUOUS: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId const ALWAYS_FALSE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId::ALWAYS_FALSE; const SMALLEST_TERMINAL: ScopedReachabilityConstraintId = ALWAYS_FALSE; +/// Maximum number of interior TDD nodes per scope. When exceeded, new constraint +/// operations return `AMBIGUOUS` to prevent exponential blowup on pathological inputs +/// (e.g., a 5000-line while loop with hundreds of if-branches). This can lead to less precise +/// reachability analysis and type narrowing. +const MAX_INTERIOR_NODES: usize = 512 * 1024; + fn singleton_to_type(db: &dyn Db, singleton: ruff_python_ast::Singleton) -> Type<'_> { let ty = match singleton { ruff_python_ast::Singleton::None => Type::none(db), @@ -588,6 +594,11 @@ impl ReachabilityConstraintsBuilder { if let Some(cached) = self.not_cache.get(&a) { return *cached; } + + if self.interiors.len() >= MAX_INTERIOR_NODES { + return AMBIGUOUS; + } + let a_node = self.interiors[a]; let if_true = self.add_not_constraint(a_node.if_true); let if_ambiguous = self.add_not_constraint(a_node.if_ambiguous); @@ -621,6 +632,10 @@ impl ReachabilityConstraintsBuilder { return *cached; } + if self.interiors.len() >= MAX_INTERIOR_NODES { + return AMBIGUOUS; + } + let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) { Ordering::Equal => { let a_node = self.interiors[a]; @@ -687,6 +702,10 @@ impl ReachabilityConstraintsBuilder { return *cached; } + if self.interiors.len() >= MAX_INTERIOR_NODES { + return AMBIGUOUS; + } + let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) { Ordering::Equal => { let a_node = self.interiors[a]; From 410902fa401afda969cc000f13be341896e6868e Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Thu, 19 Feb 2026 18:43:25 +0000 Subject: [PATCH 007/261] [`pyupgrade`] Fix handling of `typing.{io,re}` (`UP035`) (#23131) ## Summary Back in Python 3.5, the docs referred to `typing.io` as the primary location for `IO`, `TextIO`, and `BinaryIO` and `typing.re` as the primary location for `Pattern` and `Match`: - https://docs.python.org/3.5/library/typing.html#typing.io - https://docs.python.org/3.5/library/typing.html#typing.re In Python 3.6, reference to `typing.io` and `typing.re` disappeared and these types were importable directly from `typing`: - https://docs.python.org/3.6/library/typing.html#typing.IO - https://docs.python.org/3.6/library/typing.html#typing.Pattern In Python 3.9, the `typing.io` and `typing.re` namespaces were deprecated pending removal in Python 3.12. In addition, `typing.Pattern` and `typing.Match` were deprecated in favour of `re.Pattern` and `re.Match`: - https://docs.python.org/3.9/library/typing.html#typing.IO - https://docs.python.org/3.9/library/typing.html#typing.Pattern Although interestingly it implies that the deprecation of the namespaces was from Python 3.8. The pending removal version for the namespaces was updated from 3.12 to 3.13 as the deprecation warning was only in place from 3.11 - that update was backported through 3.10: - https://docs.python.org/3.10/library/typing.html#typing.IO - https://docs.python.org/3.10/library/typing.html#typing.Pattern The namespaces were removed in Python 3.13: - https://docs.python.org/3.13/library/typing.html#typing.IO - https://docs.python.org/3.13/library/typing.html#typing.Pattern On this basis, it seems we could update `UP035` for the `typing.io` and `typing.re` namespaces to target as far back as Python 3.6 as they weren't even mentioned in the docs for Python 3.6 to 3.8 and only mentioned again when soft deprecated in Python 3.9. The versions of these types in the main `typing` module have been available the whole time. As ruff only targets 3.7+, let's go for 3.7. ## Test Plan Updated snapshots. --- .../test/fixtures/pyupgrade/UP035.py | 15 ++++ .../pyupgrade/rules/deprecated_import.rs | 26 +++++- ...er__rules__pyupgrade__tests__UP035.py.snap | 90 +++++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP035.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP035.py index b9c3892a9b381..6b9073a39db27 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP035.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP035.py @@ -117,3 +117,18 @@ from typing_extensions import is_typeddict # https://github.com/astral-sh/ruff/pull/15800#pullrequestreview-2580704217 from typing_extensions import TypedDict + +# UP035 on py37+ only +from typing.io import BinaryIO + +# UP035 on py37+ only +from typing.io import IO + +# UP035 on py37+ only +from typing.io import TextIO + +# UP035 on py37+ only +from typing.re import Match + +# UP035 on py37+ only +from typing.re import Pattern diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs index 12e1b1e0393eb..86c05b9e86a99 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs @@ -109,6 +109,7 @@ fn is_relevant_module(module: &str) -> bool { | "mypy_extensions" | "typing_extensions" | "typing" + | "typing.io" | "typing.re" | "backports.strenum" ) @@ -218,6 +219,14 @@ const TYPING_EXTENSIONS_TO_TYPING_37: &[&str] = &[ // "NamedTuple", ]; +// Members of `typing.io` that were moved to `typing`. +// Note that the `typing.io` namespace has pretty much always been unnecessary. +const TYPING_IO_TO_TYPING_37: &[&str] = &["BinaryIO", "IO", "TextIO"]; + +// Members of `typing.re` that were moved to `typing`. +// Note that the `typing.re` namespace has pretty much always been unnecessary. +const TYPING_RE_TO_TYPING_37: &[&str] = &["Match", "Pattern"]; + // Python 3.8+ // Members of `mypy_extensions` that were moved to `typing`. @@ -268,7 +277,7 @@ const TYPING_TO_COLLECTIONS_ABC_39: &[&str] = &[ // Members of `typing` that were moved to `collections`. const TYPING_TO_COLLECTIONS_39: &[&str] = &["ChainMap", "Counter", "OrderedDict"]; -// Members of `typing` that were moved to `typing.re`. +// Members of `typing` that were moved to `re`. const TYPING_TO_RE_39: &[&str] = &["Match", "Pattern"]; // Members of `typing.re` that were moved to `re`. @@ -592,11 +601,22 @@ impl<'a> ImportReplacer<'a> { operations.push(operation); } } - "typing.re" if self.version >= PythonVersion::PY39 => { - if let Some(operation) = self.try_replace(TYPING_RE_TO_RE_39, "re") { + "typing.io" if self.version >= PythonVersion::PY37 => { + if let Some(operation) = self.try_replace(TYPING_IO_TO_TYPING_37, "typing") { operations.push(operation); } } + "typing.re" => { + if self.version >= PythonVersion::PY39 { + if let Some(operation) = self.try_replace(TYPING_RE_TO_RE_39, "re") { + operations.push(operation); + } + } else if self.version >= PythonVersion::PY37 { + if let Some(operation) = self.try_replace(TYPING_RE_TO_TYPING_37, "typing") { + operations.push(operation); + } + } + } "backports.strenum" if self.version >= PythonVersion::PY311 => { if let Some(operation) = self.try_replace(BACKPORTS_STR_ENUM_TO_ENUM_311, "enum") { operations.push(operation); diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap index ac901b5d6651c..66503a3ea07f0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap @@ -1163,3 +1163,93 @@ help: Import from `typing` 115 | 116 | # https://github.com/astral-sh/ruff/issues/15780 117 | from typing_extensions import is_typeddict + +UP035 [*] Import from `typing` instead: `BinaryIO` + --> UP035.py:122:1 + | +121 | # UP035 on py37+ only +122 | from typing.io import BinaryIO + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +123 | +124 | # UP035 on py37+ only + | +help: Import from `typing` +119 | from typing_extensions import TypedDict +120 | +121 | # UP035 on py37+ only + - from typing.io import BinaryIO +122 + from typing import BinaryIO +123 | +124 | # UP035 on py37+ only +125 | from typing.io import IO + +UP035 [*] Import from `typing` instead: `IO` + --> UP035.py:125:1 + | +124 | # UP035 on py37+ only +125 | from typing.io import IO + | ^^^^^^^^^^^^^^^^^^^^^^^^ +126 | +127 | # UP035 on py37+ only + | +help: Import from `typing` +122 | from typing.io import BinaryIO +123 | +124 | # UP035 on py37+ only + - from typing.io import IO +125 + from typing import IO +126 | +127 | # UP035 on py37+ only +128 | from typing.io import TextIO + +UP035 [*] Import from `typing` instead: `TextIO` + --> UP035.py:128:1 + | +127 | # UP035 on py37+ only +128 | from typing.io import TextIO + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +129 | +130 | # UP035 on py37+ only + | +help: Import from `typing` +125 | from typing.io import IO +126 | +127 | # UP035 on py37+ only + - from typing.io import TextIO +128 + from typing import TextIO +129 | +130 | # UP035 on py37+ only +131 | from typing.re import Match + +UP035 [*] Import from `re` instead: `Match` + --> UP035.py:131:1 + | +130 | # UP035 on py37+ only +131 | from typing.re import Match + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +132 | +133 | # UP035 on py37+ only + | +help: Import from `re` +128 | from typing.io import TextIO +129 | +130 | # UP035 on py37+ only + - from typing.re import Match +131 + from re import Match +132 | +133 | # UP035 on py37+ only +134 | from typing.re import Pattern + +UP035 [*] Import from `re` instead: `Pattern` + --> UP035.py:134:1 + | +133 | # UP035 on py37+ only +134 | from typing.re import Pattern + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Import from `re` +131 | from typing.re import Match +132 | +133 | # UP035 on py37+ only + - from typing.re import Pattern +134 + from re import Pattern From 1465b5de3829549b45397e9587b83ab7ac6d26d0 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Feb 2026 13:08:50 -0600 Subject: [PATCH 008/261] [`flake8-async`] Fix `in_async_context` logic (#23426) We move the logic from `Checker::in_async_context` (from when it implements `SemanticSyntaxContext`) into the similarly named method in `SemanticModel`. This affects the following rules: - ASYNC105 - ASYNC210 - ASYNC212 - ASYNC220 - ASYNC221 - ASYNC222 - ASYNC230 - ASYNC240 - ASYNC250 - ASYNC251 - UP028 Closes #23425 --- .../resources/test/fixtures/flake8_async/ASYNC250.py | 7 +++++++ crates/ruff_linter/src/checkers/ast/mod.rs | 12 +----------- ...s__flake8_async__tests__ASYNC250_ASYNC250.py.snap | 9 +++++++++ crates/ruff_python_semantic/src/model.rs | 9 +++++++-- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC250.py b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC250.py index c5b3e1fd61827..4701fa8b4978a 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC250.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC250.py @@ -20,3 +20,10 @@ def foo(): async def foo(): builtins.input("testing") # ASYNC250 fake.input("whatever") # Ok + +# Regression test for https://github.com/astral-sh/ruff/issues/23425 +import asyncio + +async def main() -> None: + input("sync") # should emit here + await asyncio.to_thread(lambda: input("async")) # but not here diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index e1b3eea62cfba..be0b789b6ed6b 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -797,17 +797,7 @@ impl SemanticSyntaxContext for Checker<'_> { } fn in_async_context(&self) -> bool { - for scope in self.semantic.current_scopes() { - match scope.kind { - ScopeKind::Class(_) | ScopeKind::Lambda(_) => return false, - ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) => return *is_async, - ScopeKind::Generator { .. } - | ScopeKind::Module - | ScopeKind::Type - | ScopeKind::DunderClassCell => {} - } - } - false + self.semantic.in_async_context() } fn in_await_allowed_context(&self) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap index 046faa42de00b..22deadaa6ce33 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap @@ -27,3 +27,12 @@ ASYNC250 Blocking call to `input()` in async context | ^^^^^^^^^^^^^^ 22 | fake.input("whatever") # Ok | + +ASYNC250 Blocking call to `input()` in async context + --> ASYNC250.py:28:5 + | +27 | async def main() -> None: +28 | input("sync") # should emit here + | ^^^^^ +29 | await asyncio.to_thread(lambda: input("async")) # but not here + | diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 78536071d718c..17d2a26fa49a5 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -1595,8 +1595,13 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the model is in an async context. pub fn in_async_context(&self) -> bool { for scope in self.current_scopes() { - if let ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) = scope.kind { - return *is_async; + match scope.kind { + ScopeKind::Class(_) | ScopeKind::Lambda(_) => return false, + ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) => return *is_async, + ScopeKind::Generator { .. } + | ScopeKind::Module + | ScopeKind::Type + | ScopeKind::DunderClassCell => {} } } false From 222574af90c5c0ca8f84c8385cf30c7c10ac2496 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:45:18 -0500 Subject: [PATCH 009/261] Expand the default rule set (#23385) ## Summary This PR adds the new default rule set in preview. This ended up being pretty non-invasive because the `DEFAULT_SELECTORS` are only used in one place where `preview` isn't set to the default value of `false`. I've currently listed each rule with a separate `RuleSelector`, which I generated with the script below. I thought about trying to be more clever by finding the smallest set of prefix selectors that yield the same rule set, but I figured this would be the easiest way to add and remove rules in the future anyway.
Script

```py import json import subprocess import tomllib from pathlib import Path from string import ascii_uppercase RULES = { rule["code"]: rule["linter"] for rule in json.loads( subprocess.run( ["ruff", "rule", "--all", "--output-format=json"], check=True, text=True, capture_output=True, ).stdout ) } for code, linter in RULES.items(): if linter == "Ruff-specific rules": RULES[code] = "Ruff" def kebab_to_pascal(s: str) -> str: return "".join(part.title() for part in s.split("-")) rules = tomllib.loads(Path("proposal.toml").read_text())["lint"]["select"] for rule in rules: linter = kebab_to_pascal(RULES[rule]) suffix = rule.lstrip(ascii_uppercase) prefix = "_" match linter: case "Flake8Comprehensions": suffix = suffix.removeprefix("4") case "Pycodestyle": prefix = "" suffix = rule case "Flake8Gettext": linter = "Flake8GetText" case "Pep8Naming": linter = "PEP8Naming" case "Pylint": prefix = "" suffix = rule.removeprefix("PL") case "Flake8Debugger": suffix = suffix.removeprefix("10") print( " " * 4, f"RuleSelector::rule(RuleCodePrefix::{linter}(" f"codes::{linter}::{prefix}{suffix}))," f" // {rule}", sep="", ) ```

## Test Plan A new CLI test showing the preview default rules. I filtered down the snapshot to just the `linter.rules.enabled` section, which isn't strictly necessary but was a lot shorter. I also tested manually in VS Code to make sure I didn't miss wiring up the preview defaults in the server: image I also tested in the playground. --- README.md | 10 +- crates/ruff/tests/cli/lint.rs | 432 ++++++++++++++++++++- crates/ruff/tests/integration_test.rs | 6 +- crates/ruff_linter/src/rule_selector.rs | 9 + crates/ruff_linter/src/settings/mod.rs | 416 ++++++++++++++++++++ crates/ruff_workspace/src/configuration.rs | 11 +- scripts/generate_mkdocs.py | 1 + 7 files changed, 878 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 408830ea535aa..1ab3c9d42652d 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ ruff check --config "lint.per-file-ignores = {'some_file.py' = ['F841']}" ``` To opt in to the latest lint rules, formatter style changes, interface updates, and more, enable -[preview mode](https://docs.astral.sh/ruff/rules/) by setting `preview = true` in your configuration +[preview mode](https://docs.astral.sh/ruff/preview/) by setting `preview = true` in your configuration file or passing `--preview` on the command line. Preview mode enables a collection of unstable features that may change prior to stabilization. @@ -311,7 +311,7 @@ for more on the linting and formatting commands, respectively. -**Ruff supports over 800 lint rules**, many of which are inspired by popular tools like Flake8, +**Ruff supports over 900 lint rules**, many of which are inspired by popular tools like Flake8, isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in Rust as a first-party feature. @@ -322,6 +322,12 @@ stylistic rules that overlap with the use of a formatter, like `ruff format` or If you're just getting started with Ruff, **the default rule set is a great place to start**: it catches a wide variety of common errors (like unused imports) with zero configuration. +In [preview](https://docs.astral.sh/ruff/preview/), Ruff enables an expanded set of default rules +that includes rules from the `B`, `UP`, and `RUF` categories, as well as many more. If you give the +new defaults a try, feel free to leave feedback in the [GitHub +discussion](https://github.com/astral-sh/ruff/discussions/23203), where you can also find the new +rule set listed in full. + Beyond the defaults, Ruff re-implements some of the most popular Flake8 plugins and related code diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index e12499b4b8fbb..8134a9af18600 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -3255,7 +3255,6 @@ fn walrus_before_py38() { .args(STDIN_BASE_OPTIONS) .args(["--stdin-filename", "test.py"]) .arg("--target-version=py37") - .arg("--preview") .arg("-") .pass_stdin(r#"(x := 1)"#), @" @@ -3939,3 +3938,434 @@ fn supported_file_extensions_preview_enabled() -> Result<()> { "); Ok(()) } + +#[test] +fn preview_default_rules() -> Result<()> { + let test = CliTest::with_settings(|_path, mut settings| { + settings.add_filter(r"(?s).*(linter\.rules\.enabled[^]]+]).*", "$1"); + settings + })?; + + test.write_file("try.py", "1")?; + + assert_cmd_snapshot!( + test.check_command().args(["--preview", "--show-settings"]), + @" + linter.rules.enabled = [ + sys-version-slice3 (YTT101), + sys-version2 (YTT102), + sys-version-cmp-str3 (YTT103), + sys-version-info0-eq3 (YTT201), + six-py3 (YTT202), + sys-version-info1-cmp-int (YTT203), + sys-version-info-minor-cmp-int (YTT204), + sys-version0 (YTT301), + sys-version-cmp-str10 (YTT302), + sys-version-slice1 (YTT303), + cancel-scope-no-checkpoint (ASYNC100), + trio-sync-call (ASYNC105), + async-zero-sleep (ASYNC115), + long-sleep-not-forever (ASYNC116), + blocking-http-call-in-async-function (ASYNC210), + create-subprocess-in-async-function (ASYNC220), + run-process-in-async-function (ASYNC221), + wait-for-process-in-async-function (ASYNC222), + blocking-open-call-in-async-function (ASYNC230), + blocking-sleep-in-async-function (ASYNC251), + exec-builtin (S102), + try-except-pass (S110), + try-except-continue (S112), + blind-except (BLE001), + unary-prefix-increment-decrement (B002), + assignment-to-os-environ (B003), + unreliable-callable-check (B004), + strip-with-multi-characters (B005), + mutable-argument-default (B006), + function-call-in-default-argument (B008), + get-attr-with-constant (B009), + set-attr-with-constant (B010), + jump-statement-in-finally (B012), + redundant-tuple-in-exception-handler (B013), + duplicate-handler-exception (B014), + useless-comparison (B015), + raise-literal (B016), + assert-raises-exception (B017), + useless-expression (B018), + cached-instance-method (B019), + loop-variable-overrides-iterator (B020), + f-string-docstring (B021), + useless-contextlib-suppress (B022), + function-uses-loop-variable (B023), + duplicate-try-block-exception (B025), + star-arg-unpacking-after-keyword-arg (B026), + except-with-empty-tuple (B029), + except-with-non-exception-classes (B030), + reuse-of-groupby-generator (B031), + unintentional-type-annotation (B032), + duplicate-value (B033), + static-key-dict-comprehension (B035), + mutable-contextvar-default (B039), + unnecessary-generator-list (C400), + unnecessary-generator-set (C401), + unnecessary-generator-dict (C402), + unnecessary-list-comprehension-set (C403), + unnecessary-list-comprehension-dict (C404), + unnecessary-literal-set (C405), + unnecessary-literal-dict (C406), + unnecessary-collection-call (C408), + unnecessary-literal-within-tuple-call (C409), + unnecessary-literal-within-list-call (C410), + unnecessary-list-call (C411), + unnecessary-call-around-sorted (C413), + unnecessary-double-cast-or-process (C414), + unnecessary-subscript-reversal (C415), + unnecessary-map (C417), + unnecessary-literal-within-dict-call (C418), + unnecessary-comprehension-in-call (C419), + call-datetime-without-tzinfo (DTZ001), + call-datetime-today (DTZ002), + call-datetime-utcnow (DTZ003), + call-datetime-utcfromtimestamp (DTZ004), + call-datetime-now-without-tzinfo (DTZ005), + call-datetime-fromtimestamp (DTZ006), + call-datetime-strptime-without-zone (DTZ007), + call-date-today (DTZ011), + call-date-fromtimestamp (DTZ012), + datetime-min-max (DTZ901), + debugger (T100), + shebang-not-executable (EXE001), + shebang-missing-executable-file (EXE002), + shebang-leading-whitespace (EXE004), + shebang-not-first-line (EXE005), + future-rewritable-type-annotation (FA100), + future-required-type-annotation (FA102), + f-string-in-get-text-func-call (INT001), + format-in-get-text-func-call (INT002), + printf-in-get-text-func-call (INT003), + direct-logger-instantiation (LOG001), + invalid-get-logger-argument (LOG002), + undocumented-warn (LOG009), + exc-info-outside-except-handler (LOG014), + root-logger-call (LOG015), + logging-warn (G010), + logging-extra-attr-clash (G101), + logging-exc-info (G201), + logging-redundant-exc-info (G202), + unnecessary-placeholder (PIE790), + duplicate-class-field-definition (PIE794), + non-unique-enums (PIE796), + unnecessary-spread (PIE800), + unnecessary-dict-kwargs (PIE804), + reimplemented-container-builtin (PIE807), + unnecessary-range-start (PIE808), + multiple-starts-ends-with (PIE810), + unprefixed-type-param (PYI001), + complex-if-statement-in-stub (PYI002), + unrecognized-version-info-check (PYI003), + patch-version-comparison (PYI004), + wrong-tuple-length-version-comparison (PYI005), + bad-version-info-comparison (PYI006), + unrecognized-platform-check (PYI007), + unrecognized-platform-name (PYI008), + pass-statement-stub-body (PYI009), + non-empty-stub-body (PYI010), + pass-in-class-body (PYI012), + ellipsis-in-non-empty-class-body (PYI013), + assignment-default-in-stub (PYI015), + duplicate-union-member (PYI016), + complex-assignment-in-stub (PYI017), + unused-private-type-var (PYI018), + custom-type-var-for-self (PYI019), + quoted-annotation-in-stub (PYI020), + unaliased-collections-abc-set-import (PYI025), + type-alias-without-annotation (PYI026), + str-or-repr-defined-in-stub (PYI029), + unnecessary-literal-union (PYI030), + any-eq-ne-annotation (PYI032), + type-comment-in-stub (PYI033), + non-self-return-type (PYI034), + unassigned-special-variable-in-stub (PYI035), + bad-exit-annotation (PYI036), + redundant-numeric-union (PYI041), + snake-case-type-alias (PYI042), + t-suffixed-type-alias (PYI043), + future-annotations-in-stub (PYI044), + iter-method-return-iterable (PYI045), + unused-private-protocol (PYI046), + unused-private-type-alias (PYI047), + stub-body-multiple-statements (PYI048), + unused-private-typed-dict (PYI049), + no-return-argument-annotation-in-stub (PYI050), + unannotated-assignment-in-stub (PYI052), + unnecessary-type-union (PYI055), + byte-string-usage (PYI057), + generator-return-from-iter-method (PYI058), + generic-not-last-base-class (PYI059), + redundant-none-literal (PYI061), + duplicate-literal-member (PYI062), + pep484-style-positional-only-parameter (PYI063), + redundant-final-literal (PYI064), + bad-version-info-order (PYI066), + pytest-raises-without-exception (PT010), + pytest-duplicate-parametrize-test-cases (PT014), + pytest-deprecated-yield-fixture (PT020), + pytest-erroneous-use-fixtures-on-fixture (PT025), + pytest-use-fixtures-without-parameters (PT026), + pytest-warns-with-multiple-statements (PT031), + unnecessary-return-none (RET501), + unnecessary-assign (RET504), + duplicate-isinstance-call (SIM101), + collapsible-if (SIM102), + needless-bool (SIM103), + return-in-try-except-finally (SIM107), + enumerate-for-loop (SIM113), + if-with-same-arms (SIM114), + open-file-with-context-handler (SIM115), + multiple-with-statements (SIM117), + in-dict-keys (SIM118), + negate-equal-op (SIM201), + negate-not-equal-op (SIM202), + double-negation (SIM208), + if-expr-with-true-false (SIM210), + if-expr-with-false-true (SIM211), + expr-and-not-expr (SIM220), + expr-or-not-expr (SIM221), + expr-or-true (SIM222), + expr-and-false (SIM223), + if-else-block-instead-of-dict-get (SIM401), + split-static-string (SIM905), + zip-dict-keys-and-values (SIM911), + runtime-import-in-type-checking-block (TC004), + empty-type-checking-block (TC005), + unquoted-type-alias (TC007), + runtime-string-union (TC010), + py-path (PTH124), + invalid-pathlib-with-suffix (PTH210), + static-join-to-f-string (FLY002), + unsorted-imports (I001), + invalid-module-name (N999), + unnecessary-list-cast (PERF101), + incorrect-dict-iterator (PERF102), + manual-list-comprehension (PERF401), + manual-list-copy (PERF402), + manual-dict-comprehension (PERF403), + bare-except (E722), + io-error (E902), + invalid-escape-sequence (W605), + empty-docstring (D419), + unused-import (F401), + import-shadowed-by-loop-var (F402), + late-future-import (F404), + future-feature-not-defined (F407), + percent-format-invalid-format (F501), + percent-format-expected-mapping (F502), + percent-format-expected-sequence (F503), + percent-format-extra-named-arguments (F504), + percent-format-missing-argument (F505), + percent-format-mixed-positional-and-named (F506), + percent-format-positional-count-mismatch (F507), + percent-format-star-requires-sequence (F508), + percent-format-unsupported-format-character (F509), + string-dot-format-invalid-format (F521), + string-dot-format-extra-named-arguments (F522), + string-dot-format-extra-positional-arguments (F523), + string-dot-format-missing-arguments (F524), + string-dot-format-mixing-automatic (F525), + f-string-missing-placeholders (F541), + multi-value-repeated-key-literal (F601), + multi-value-repeated-key-variable (F602), + expressions-in-star-assignment (F621), + multiple-starred-expressions (F622), + assert-tuple (F631), + is-literal (F632), + invalid-print-syntax (F633), + if-tuple (F634), + break-outside-loop (F701), + continue-outside-loop (F702), + yield-outside-function (F704), + return-outside-function (F706), + default-except-not-last (F707), + redefined-while-unused (F811), + undefined-name (F821), + undefined-export (F822), + undefined-local (F823), + unused-variable (F841), + unused-annotation (F842), + raise-not-implemented (F901), + invalid-mock-access (PGH005), + type-name-incorrect-variance (PLC0105), + type-bivariance (PLC0131), + type-param-name-mismatch (PLC0132), + single-string-slots (PLC0205), + dict-index-missing-items (PLC0206), + iteration-over-set (PLC0208), + useless-import-alias (PLC0414), + unnecessary-direct-lambda-call (PLC3002), + yield-in-init (PLE0100), + return-in-init (PLE0101), + nonlocal-and-global (PLE0115), + continue-in-finally (PLE0116), + nonlocal-without-binding (PLE0117), + load-before-global-declaration (PLE0118), + invalid-length-return-type (PLE0303), + invalid-index-return-type (PLE0305), + invalid-str-return-type (PLE0307), + invalid-bytes-return-type (PLE0308), + invalid-hash-return-type (PLE0309), + invalid-all-object (PLE0604), + invalid-all-format (PLE0605), + potential-index-error (PLE0643), + misplaced-bare-raise (PLE0704), + repeated-keyword-argument (PLE1132), + await-outside-async (PLE1142), + logging-too-many-args (PLE1205), + logging-too-few-args (PLE1206), + bad-string-format-character (PLE1300), + bad-string-format-type (PLE1307), + bad-str-strip-call (PLE1310), + invalid-envvar-value (PLE1507), + singledispatch-method (PLE1519), + singledispatchmethod-function (PLE1520), + yield-from-in-async-function (PLE1700), + bidirectional-unicode (PLE2502), + invalid-character-backspace (PLE2510), + invalid-character-sub (PLE2512), + invalid-character-esc (PLE2513), + invalid-character-nul (PLE2514), + invalid-character-zero-width-space (PLE2515), + comparison-with-itself (PLR0124), + comparison-of-constant (PLR0133), + property-with-parameters (PLR0206), + manual-from-import (PLR0402), + redefined-argument-from-local (PLR1704), + useless-return (PLR1711), + repeated-equality-comparison (PLR1714), + boolean-chained-comparison (PLR1716), + sys-exit-alias (PLR1722), + if-stmt-min-max (PLR1730), + unnecessary-dict-index-lookup (PLR1733), + unnecessary-list-index-lookup (PLR1736), + empty-comment (PLR2044), + useless-else-on-loop (PLW0120), + self-assigning-variable (PLW0127), + redeclared-assigned-name (PLW0128), + assert-on-string-literal (PLW0129), + named-expr-without-context (PLW0131), + useless-exception-statement (PLW0133), + nan-comparison (PLW0177), + bad-staticmethod-argument (PLW0211), + super-without-brackets (PLW0245), + import-self (PLW0406), + global-variable-not-assigned (PLW0602), + global-at-module-level (PLW0604), + self-or-cls-assignment (PLW0642), + binary-op-exception (PLW0711), + bad-open-mode (PLW1501), + shallow-copy-environ (PLW1507), + invalid-envvar-default (PLW1508), + subprocess-popen-preexec-fn (PLW1509), + subprocess-run-without-check (PLW1510), + useless-with-lock (PLW2101), + useless-metaclass-type (UP001), + type-of-primitive (UP003), + useless-object-inheritance (UP004), + deprecated-unittest-alias (UP005), + non-pep585-annotation (UP006), + non-pep604-annotation-union (UP007), + super-call-with-parameters (UP008), + utf8-encoding-declaration (UP009), + unnecessary-future-import (UP010), + lru-cache-without-parameters (UP011), + unnecessary-encode-utf8 (UP012), + convert-named-tuple-functional-to-class (UP014), + datetime-timezone-utc (UP017), + native-literals (UP018), + typing-text-str-alias (UP019), + open-alias (UP020), + replace-universal-newlines (UP021), + replace-stdout-stderr (UP022), + deprecated-c-element-tree (UP023), + os-error-alias (UP024), + unicode-kind-prefix (UP025), + deprecated-mock-import (UP026), + yield-in-for-loop (UP028), + unnecessary-builtin-import (UP029), + format-literals (UP030), + printf-string-formatting (UP031), + f-string (UP032), + lru-cache-with-maxsize-none (UP033), + extraneous-parentheses (UP034), + deprecated-import (UP035), + outdated-version-block (UP036), + quoted-annotation (UP037), + unnecessary-class-parentheses (UP039), + non-pep695-type-alias (UP040), + timeout-error-alias (UP041), + unnecessary-default-type-args (UP043), + non-pep646-unpack (UP044), + non-pep604-annotation-optional (UP045), + non-pep695-generic-class (UP046), + non-pep695-generic-function (UP047), + private-type-parameter (UP049), + useless-class-metaclass-type (UP050), + print-empty-string (FURB105), + for-loop-writes (FURB122), + readlines-in-for (FURB129), + check-and-remove-from-set (FURB132), + if-expr-min-max (FURB136), + verbose-decimal-constructor (FURB157), + bit-count (FURB161), + fromisoformat-replace-z (FURB162), + redundant-log-base (FURB163), + int-on-sliced-str (FURB166), + regex-flag-alias (FURB167), + isinstance-type-none (FURB168), + type-none-comparison (FURB169), + implicit-cwd (FURB177), + hashlib-digest-hex (FURB181), + slice-to-remove-prefix-or-suffix (FURB188), + zip-instead-of-pairwise (RUF007), + mutable-dataclass-default (RUF008), + function-call-in-dataclass-default-argument (RUF009), + explicit-f-string-type-conversion (RUF010), + mutable-class-default (RUF012), + implicit-optional (RUF013), + unnecessary-iterable-allocation-for-first-element (RUF015), + invalid-index-type (RUF016), + quadratic-list-summation (RUF017), + assignment-in-assert (RUF018), + unnecessary-key-check (RUF019), + never-union (RUF020), + unsorted-dunder-all (RUF022), + unsorted-dunder-slots (RUF023), + mutable-fromkeys-value (RUF024), + default-factory-kwarg (RUF026), + invalid-formatter-suppression-comment (RUF028), + assert-with-print-message (RUF030), + decimal-from-float-literal (RUF032), + post-init-default (RUF033), + useless-if-else (RUF034), + invalid-assert-message-literal-argument (RUF040), + unnecessary-nested-literal (RUF041), + unnecessary-cast-to-int (RUF046), + map-int-version-parsing (RUF048), + dataclass-enum (RUF049), + if-key-in-dict-del (RUF051), + class-with-mixed-type-vars (RUF053), + unnecessary-round (RUF057), + starmap-zip (RUF058), + unused-unpacked-variable (RUF059), + unused-noqa (RUF100), + redirected-noqa (RUF101), + invalid-pyproject-toml (RUF200), + raise-vanilla-class (TRY002), + type-check-without-type-error (TRY004), + verbose-raise (TRY201), + useless-try-except (TRY203), + try-consider-else (TRY300), + verbose-log-message (TRY401), + ] + ", + ); + Ok(()) +} diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 8149004565770..cc6678882a5bb 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -876,7 +876,9 @@ fn parse_error_not_included() { #[test] fn full_output_preview() { - let mut cmd = RuffCheck::default().args(["--preview"]).build(); + let mut cmd = RuffCheck::default() + .args(["--preview", "--select=E741"]) + .build(); assert_cmd_snapshot!(cmd .pass_stdin("l = 1"), @" success: false @@ -907,7 +909,7 @@ preview = true ", )?; let mut cmd = RuffCheck::default().config(&pyproject_toml).build(); - assert_cmd_snapshot!(cmd.pass_stdin("l = 1"), @" + assert_cmd_snapshot!(cmd.arg("--select=E741").pass_stdin("l = 1"), @" success: false exit_code: 1 ----- stdout ----- diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index b3eee0d8370b3..b3e08407e94ea 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -35,6 +35,15 @@ pub enum RuleSelector { }, } +impl RuleSelector { + pub(crate) const fn rule(prefix: RuleCodePrefix) -> Self { + Self::Rule { + prefix, + redirected_from: None, + } + } +} + impl From for RuleSelector { fn from(linter: Linter) -> Self { Self::Linter(linter) diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 75f9bfe680408..b098d891d97e4 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -371,6 +371,422 @@ pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ }, ]; +#[rustfmt::skip] +pub const PREVIEW_DEFAULT_SELECTORS: &[RuleSelector] = &[ + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_100)), // ASYNC100 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_105)), // ASYNC105 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_115)), // ASYNC115 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_116)), // ASYNC116 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_210)), // ASYNC210 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_220)), // ASYNC220 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_221)), // ASYNC221 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_222)), // ASYNC222 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_230)), // ASYNC230 + RuleSelector::rule(RuleCodePrefix::Flake8Async(codes::Flake8Async::_251)), // ASYNC251 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_002)), // B002 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_003)), // B003 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_004)), // B004 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_005)), // B005 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_006)), // B006 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_008)), // B008 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_009)), // B009 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_010)), // B010 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_012)), // B012 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_013)), // B013 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_014)), // B014 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_015)), // B015 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_016)), // B016 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_017)), // B017 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_018)), // B018 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_019)), // B019 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_020)), // B020 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_021)), // B021 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_022)), // B022 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_023)), // B023 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_025)), // B025 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_026)), // B026 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_029)), // B029 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_030)), // B030 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_031)), // B031 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_032)), // B032 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_033)), // B033 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_035)), // B035 + RuleSelector::rule(RuleCodePrefix::Flake8Bugbear(codes::Flake8Bugbear::_039)), // B039 + RuleSelector::rule(RuleCodePrefix::Flake8BlindExcept(codes::Flake8BlindExcept::_001)), // BLE001 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_00)), // C400 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_01)), // C401 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_02)), // C402 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_03)), // C403 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_04)), // C404 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_05)), // C405 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_06)), // C406 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_08)), // C408 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_09)), // C409 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_10)), // C410 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_11)), // C411 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_13)), // C413 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_14)), // C414 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_15)), // C415 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_17)), // C417 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_18)), // C418 + RuleSelector::rule(RuleCodePrefix::Flake8Comprehensions(codes::Flake8Comprehensions::_19)), // C419 + RuleSelector::rule(RuleCodePrefix::Pydocstyle(codes::Pydocstyle::_419)), // D419 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_001)), // DTZ001 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_002)), // DTZ002 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_003)), // DTZ003 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_004)), // DTZ004 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_005)), // DTZ005 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_006)), // DTZ006 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_007)), // DTZ007 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_011)), // DTZ011 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_012)), // DTZ012 + RuleSelector::rule(RuleCodePrefix::Flake8Datetimez(codes::Flake8Datetimez::_901)), // DTZ901 + RuleSelector::rule(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E722)), // E722 + RuleSelector::rule(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E902)), // E902 + RuleSelector::rule(RuleCodePrefix::Flake8Executable(codes::Flake8Executable::_001)), // EXE001 + RuleSelector::rule(RuleCodePrefix::Flake8Executable(codes::Flake8Executable::_002)), // EXE002 + RuleSelector::rule(RuleCodePrefix::Flake8Executable(codes::Flake8Executable::_004)), // EXE004 + RuleSelector::rule(RuleCodePrefix::Flake8Executable(codes::Flake8Executable::_005)), // EXE005 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_401)), // F401 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_402)), // F402 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_404)), // F404 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_407)), // F407 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_501)), // F501 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_502)), // F502 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_503)), // F503 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_504)), // F504 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_505)), // F505 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_506)), // F506 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_507)), // F507 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_508)), // F508 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_509)), // F509 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_521)), // F521 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_522)), // F522 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_523)), // F523 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_524)), // F524 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_525)), // F525 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_541)), // F541 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_601)), // F601 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_602)), // F602 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_621)), // F621 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_622)), // F622 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_631)), // F631 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_632)), // F632 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_633)), // F633 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_634)), // F634 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_701)), // F701 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_702)), // F702 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_704)), // F704 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_706)), // F706 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_707)), // F707 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_811)), // F811 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_821)), // F821 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_822)), // F822 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_823)), // F823 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_841)), // F841 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_842)), // F842 + RuleSelector::rule(RuleCodePrefix::Pyflakes(codes::Pyflakes::_901)), // F901 + RuleSelector::rule(RuleCodePrefix::Flake8FutureAnnotations(codes::Flake8FutureAnnotations::_100)), // FA100 + RuleSelector::rule(RuleCodePrefix::Flake8FutureAnnotations(codes::Flake8FutureAnnotations::_102)), // FA102 + RuleSelector::rule(RuleCodePrefix::Flynt(codes::Flynt::_002)), // FLY002 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_105)), // FURB105 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_122)), // FURB122 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_129)), // FURB129 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_132)), // FURB132 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_136)), // FURB136 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_157)), // FURB157 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_161)), // FURB161 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_162)), // FURB162 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_163)), // FURB163 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_166)), // FURB166 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_167)), // FURB167 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_168)), // FURB168 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_169)), // FURB169 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_177)), // FURB177 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_181)), // FURB181 + RuleSelector::rule(RuleCodePrefix::Refurb(codes::Refurb::_188)), // FURB188 + RuleSelector::rule(RuleCodePrefix::Flake8LoggingFormat(codes::Flake8LoggingFormat::_010)), // G010 + RuleSelector::rule(RuleCodePrefix::Flake8LoggingFormat(codes::Flake8LoggingFormat::_101)), // G101 + RuleSelector::rule(RuleCodePrefix::Flake8LoggingFormat(codes::Flake8LoggingFormat::_201)), // G201 + RuleSelector::rule(RuleCodePrefix::Flake8LoggingFormat(codes::Flake8LoggingFormat::_202)), // G202 + RuleSelector::rule(RuleCodePrefix::Isort(codes::Isort::_001)), // I001 + RuleSelector::rule(RuleCodePrefix::Flake8GetText(codes::Flake8GetText::_001)), // INT001 + RuleSelector::rule(RuleCodePrefix::Flake8GetText(codes::Flake8GetText::_002)), // INT002 + RuleSelector::rule(RuleCodePrefix::Flake8GetText(codes::Flake8GetText::_003)), // INT003 + RuleSelector::rule(RuleCodePrefix::Flake8Logging(codes::Flake8Logging::_001)), // LOG001 + RuleSelector::rule(RuleCodePrefix::Flake8Logging(codes::Flake8Logging::_002)), // LOG002 + RuleSelector::rule(RuleCodePrefix::Flake8Logging(codes::Flake8Logging::_009)), // LOG009 + RuleSelector::rule(RuleCodePrefix::Flake8Logging(codes::Flake8Logging::_014)), // LOG014 + RuleSelector::rule(RuleCodePrefix::Flake8Logging(codes::Flake8Logging::_015)), // LOG015 + RuleSelector::rule(RuleCodePrefix::PEP8Naming(codes::PEP8Naming::_999)), // N999 + RuleSelector::rule(RuleCodePrefix::Perflint(codes::Perflint::_101)), // PERF101 + RuleSelector::rule(RuleCodePrefix::Perflint(codes::Perflint::_102)), // PERF102 + RuleSelector::rule(RuleCodePrefix::Perflint(codes::Perflint::_401)), // PERF401 + RuleSelector::rule(RuleCodePrefix::Perflint(codes::Perflint::_402)), // PERF402 + RuleSelector::rule(RuleCodePrefix::Perflint(codes::Perflint::_403)), // PERF403 + RuleSelector::rule(RuleCodePrefix::PygrepHooks(codes::PygrepHooks::_005)), // PGH005 + RuleSelector::rule(RuleCodePrefix::Flake8Pie(codes::Flake8Pie::_790)), // PIE790 + RuleSelector::rule(RuleCodePrefix::Flake8Pie(codes::Flake8Pie::_794)), // PIE794 + RuleSelector::rule(RuleCodePrefix::Flake8Pie(codes::Flake8Pie::_796)), // PIE796 + RuleSelector::rule(RuleCodePrefix::Flake8Pie(codes::Flake8Pie::_800)), // PIE800 + RuleSelector::rule(RuleCodePrefix::Flake8Pie(codes::Flake8Pie::_804)), // PIE804 + RuleSelector::rule(RuleCodePrefix::Flake8Pie(codes::Flake8Pie::_807)), // PIE807 + RuleSelector::rule(RuleCodePrefix::Flake8Pie(codes::Flake8Pie::_808)), // PIE808 + RuleSelector::rule(RuleCodePrefix::Flake8Pie(codes::Flake8Pie::_810)), // PIE810 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::C0105)), // PLC0105 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::C0131)), // PLC0131 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::C0132)), // PLC0132 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::C0205)), // PLC0205 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::C0206)), // PLC0206 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::C0208)), // PLC0208 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::C0414)), // PLC0414 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::C3002)), // PLC3002 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0100)), // PLE0100 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0101)), // PLE0101 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0115)), // PLE0115 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0116)), // PLE0116 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0117)), // PLE0117 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0118)), // PLE0118 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0303)), // PLE0303 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0305)), // PLE0305 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0307)), // PLE0307 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0308)), // PLE0308 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0309)), // PLE0309 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0604)), // PLE0604 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0605)), // PLE0605 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0643)), // PLE0643 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E0704)), // PLE0704 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1132)), // PLE1132 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1142)), // PLE1142 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1205)), // PLE1205 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1206)), // PLE1206 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1300)), // PLE1300 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1307)), // PLE1307 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1310)), // PLE1310 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1507)), // PLE1507 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1519)), // PLE1519 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1520)), // PLE1520 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E1700)), // PLE1700 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E2502)), // PLE2502 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E2510)), // PLE2510 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E2512)), // PLE2512 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E2513)), // PLE2513 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E2514)), // PLE2514 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::E2515)), // PLE2515 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R0124)), // PLR0124 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R0133)), // PLR0133 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R0206)), // PLR0206 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R0402)), // PLR0402 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R1704)), // PLR1704 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R1711)), // PLR1711 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R1714)), // PLR1714 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R1716)), // PLR1716 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R1722)), // PLR1722 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R1730)), // PLR1730 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R1733)), // PLR1733 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R1736)), // PLR1736 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::R2044)), // PLR2044 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0120)), // PLW0120 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0127)), // PLW0127 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0128)), // PLW0128 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0129)), // PLW0129 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0131)), // PLW0131 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0133)), // PLW0133 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0177)), // PLW0177 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0211)), // PLW0211 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0245)), // PLW0245 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0406)), // PLW0406 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0602)), // PLW0602 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0604)), // PLW0604 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0642)), // PLW0642 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W0711)), // PLW0711 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W1501)), // PLW1501 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W1507)), // PLW1507 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W1508)), // PLW1508 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W1509)), // PLW1509 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W1510)), // PLW1510 + RuleSelector::rule(RuleCodePrefix::Pylint(codes::Pylint::W2101)), // PLW2101 + RuleSelector::rule(RuleCodePrefix::Flake8PytestStyle(codes::Flake8PytestStyle::_010)), // PT010 + RuleSelector::rule(RuleCodePrefix::Flake8PytestStyle(codes::Flake8PytestStyle::_014)), // PT014 + RuleSelector::rule(RuleCodePrefix::Flake8PytestStyle(codes::Flake8PytestStyle::_020)), // PT020 + RuleSelector::rule(RuleCodePrefix::Flake8PytestStyle(codes::Flake8PytestStyle::_025)), // PT025 + RuleSelector::rule(RuleCodePrefix::Flake8PytestStyle(codes::Flake8PytestStyle::_026)), // PT026 + RuleSelector::rule(RuleCodePrefix::Flake8PytestStyle(codes::Flake8PytestStyle::_031)), // PT031 + RuleSelector::rule(RuleCodePrefix::Flake8UsePathlib(codes::Flake8UsePathlib::_124)), // PTH124 + RuleSelector::rule(RuleCodePrefix::Flake8UsePathlib(codes::Flake8UsePathlib::_210)), // PTH210 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_001)), // PYI001 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_002)), // PYI002 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_003)), // PYI003 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_004)), // PYI004 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_005)), // PYI005 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_006)), // PYI006 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_007)), // PYI007 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_008)), // PYI008 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_009)), // PYI009 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_010)), // PYI010 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_012)), // PYI012 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_013)), // PYI013 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_015)), // PYI015 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_016)), // PYI016 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_017)), // PYI017 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_018)), // PYI018 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_019)), // PYI019 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_020)), // PYI020 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_025)), // PYI025 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_026)), // PYI026 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_029)), // PYI029 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_030)), // PYI030 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_032)), // PYI032 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_033)), // PYI033 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_034)), // PYI034 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_035)), // PYI035 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_036)), // PYI036 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_041)), // PYI041 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_042)), // PYI042 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_043)), // PYI043 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_044)), // PYI044 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_045)), // PYI045 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_046)), // PYI046 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_047)), // PYI047 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_048)), // PYI048 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_049)), // PYI049 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_050)), // PYI050 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_052)), // PYI052 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_055)), // PYI055 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_057)), // PYI057 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_058)), // PYI058 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_059)), // PYI059 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_061)), // PYI061 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_062)), // PYI062 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_063)), // PYI063 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_064)), // PYI064 + RuleSelector::rule(RuleCodePrefix::Flake8Pyi(codes::Flake8Pyi::_066)), // PYI066 + RuleSelector::rule(RuleCodePrefix::Flake8Return(codes::Flake8Return::_501)), // RET501 + RuleSelector::rule(RuleCodePrefix::Flake8Return(codes::Flake8Return::_504)), // RET504 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_007)), // RUF007 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_008)), // RUF008 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_009)), // RUF009 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_010)), // RUF010 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_012)), // RUF012 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_013)), // RUF013 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_015)), // RUF015 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_016)), // RUF016 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_017)), // RUF017 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_018)), // RUF018 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_019)), // RUF019 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_020)), // RUF020 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_022)), // RUF022 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_023)), // RUF023 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_024)), // RUF024 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_026)), // RUF026 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_028)), // RUF028 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_030)), // RUF030 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_032)), // RUF032 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_033)), // RUF033 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_034)), // RUF034 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_040)), // RUF040 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_041)), // RUF041 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_046)), // RUF046 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_048)), // RUF048 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_049)), // RUF049 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_051)), // RUF051 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_053)), // RUF053 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_057)), // RUF057 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_058)), // RUF058 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_059)), // RUF059 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_100)), // RUF100 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_101)), // RUF101 + RuleSelector::rule(RuleCodePrefix::Ruff(codes::Ruff::_200)), // RUF200 + RuleSelector::rule(RuleCodePrefix::Flake8Bandit(codes::Flake8Bandit::_102)), // S102 + RuleSelector::rule(RuleCodePrefix::Flake8Bandit(codes::Flake8Bandit::_110)), // S110 + RuleSelector::rule(RuleCodePrefix::Flake8Bandit(codes::Flake8Bandit::_112)), // S112 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_101)), // SIM101 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_102)), // SIM102 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_103)), // SIM103 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_107)), // SIM107 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_113)), // SIM113 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_114)), // SIM114 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_115)), // SIM115 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_117)), // SIM117 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_118)), // SIM118 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_201)), // SIM201 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_202)), // SIM202 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_208)), // SIM208 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_210)), // SIM210 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_211)), // SIM211 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_220)), // SIM220 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_221)), // SIM221 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_222)), // SIM222 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_223)), // SIM223 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_401)), // SIM401 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_905)), // SIM905 + RuleSelector::rule(RuleCodePrefix::Flake8Simplify(codes::Flake8Simplify::_911)), // SIM911 + RuleSelector::rule(RuleCodePrefix::Flake8Debugger(codes::Flake8Debugger::_0)), // T100 + RuleSelector::rule(RuleCodePrefix::Flake8TypeChecking(codes::Flake8TypeChecking::_004)), // TC004 + RuleSelector::rule(RuleCodePrefix::Flake8TypeChecking(codes::Flake8TypeChecking::_005)), // TC005 + RuleSelector::rule(RuleCodePrefix::Flake8TypeChecking(codes::Flake8TypeChecking::_007)), // TC007 + RuleSelector::rule(RuleCodePrefix::Flake8TypeChecking(codes::Flake8TypeChecking::_010)), // TC010 + RuleSelector::rule(RuleCodePrefix::Tryceratops(codes::Tryceratops::_002)), // TRY002 + RuleSelector::rule(RuleCodePrefix::Tryceratops(codes::Tryceratops::_004)), // TRY004 + RuleSelector::rule(RuleCodePrefix::Tryceratops(codes::Tryceratops::_201)), // TRY201 + RuleSelector::rule(RuleCodePrefix::Tryceratops(codes::Tryceratops::_203)), // TRY203 + RuleSelector::rule(RuleCodePrefix::Tryceratops(codes::Tryceratops::_300)), // TRY300 + RuleSelector::rule(RuleCodePrefix::Tryceratops(codes::Tryceratops::_401)), // TRY401 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_001)), // UP001 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_003)), // UP003 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_004)), // UP004 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_005)), // UP005 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_006)), // UP006 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_007)), // UP007 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_008)), // UP008 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_009)), // UP009 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_010)), // UP010 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_011)), // UP011 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_012)), // UP012 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_014)), // UP014 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_017)), // UP017 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_018)), // UP018 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_019)), // UP019 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_020)), // UP020 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_021)), // UP021 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_022)), // UP022 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_023)), // UP023 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_024)), // UP024 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_025)), // UP025 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_026)), // UP026 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_028)), // UP028 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_029)), // UP029 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_030)), // UP030 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_031)), // UP031 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_032)), // UP032 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_033)), // UP033 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_034)), // UP034 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_035)), // UP035 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_036)), // UP036 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_037)), // UP037 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_039)), // UP039 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_040)), // UP040 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_041)), // UP041 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_043)), // UP043 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_044)), // UP044 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_045)), // UP045 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_046)), // UP046 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_047)), // UP047 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_049)), // UP049 + RuleSelector::rule(RuleCodePrefix::Pyupgrade(codes::Pyupgrade::_050)), // UP050 + RuleSelector::rule(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::W605)), // W605 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_101)), // YTT101 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_102)), // YTT102 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_103)), // YTT103 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_201)), // YTT201 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_202)), // YTT202 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_203)), // YTT203 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_204)), // YTT204 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_301)), // YTT301 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_302)), // YTT302 + RuleSelector::rule(RuleCodePrefix::Flake82020(codes::Flake82020::_303)), // YTT303 +]; + pub const TASK_TAGS: &[&str] = &["TODO", "FIXME", "XXX"]; pub static DUMMY_VARIABLE_RGX: LazyLock = diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 5c204b3a0078c..48a580e50961d 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -33,7 +33,8 @@ use ruff_linter::settings::types::{ RequiredVersion, UnsafeFixes, }; use ruff_linter::settings::{ - DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, TASK_TAGS, TargetVersion, + DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, PREVIEW_DEFAULT_SELECTORS, TASK_TAGS, + TargetVersion, }; use ruff_linter::{ RuleSelector, fs, warn_user_once, warn_user_once_by_id, warn_user_once_by_message, @@ -831,8 +832,14 @@ impl LintConfiguration { require_explicit: self.explicit_preview_rules.unwrap_or_default(), }; + let selectors = if preview.mode.is_enabled() { + PREVIEW_DEFAULT_SELECTORS + } else { + DEFAULT_SELECTORS + }; + // The select_set keeps track of which rules have been selected. - let mut select_set: RuleSet = DEFAULT_SELECTORS + let mut select_set: RuleSet = selectors .iter() .flat_map(|selector| selector.rules(&preview)) .collect(); diff --git a/scripts/generate_mkdocs.py b/scripts/generate_mkdocs.py index d78307b28e42c..c043ff79ad819 100644 --- a/scripts/generate_mkdocs.py +++ b/scripts/generate_mkdocs.py @@ -73,6 +73,7 @@ class Section(NamedTuple): "https://docs.astral.sh/ruff/rules/": "rules.md", "https://docs.astral.sh/ruff/settings/": "settings.md", "#whos-using-ruff": "https://github.com/astral-sh/ruff#whos-using-ruff", + "https://docs.astral.sh/ruff/preview/": "preview.md", } From d1b544393ae9cddd8e48ebee8dbfd54bda89f375 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Thu, 19 Feb 2026 12:21:11 -0800 Subject: [PATCH 010/261] Add extension mapping to configuration file options (#23384) New `extension` configuration option takes a dictionary mapping custom file extensions (keys) to languages by name (values). Eg, ```toml [tool.ruff] extension = {qmd="markdown"} ``` Issue #23204 --- crates/ruff/tests/integration_test.rs | 36 ++++++++++++++++++++++ crates/ruff_workspace/src/configuration.rs | 5 +-- crates/ruff_workspace/src/options.rs | 16 +++++++++- ruff.schema.json | 19 ++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index cc6678882a5bb..44f644011a696 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -629,6 +629,42 @@ fn stdin_override_parser_py() { "); } +#[test] +fn stdin_override_parser_py_config() -> Result<()> { + let tempdir = TempDir::new()?; + let pyproject_toml = tempdir.path().join("pyproject.toml"); + fs::write( + &pyproject_toml, + r#" +[tool.ruff] +extension = {ipynb="python"} +"#, + )?; + let mut cmd = RuffCheck::default() + .config(&pyproject_toml) + .args(["--stdin-filename", "F401.ipynb"]) + .build(); + assert_cmd_snapshot!(cmd + .pass_stdin("import os\n"), @" + success: false + exit_code: 1 + ----- stdout ----- + F401 [*] `os` imported but unused + --> F401.ipynb:1:8 + | + 1 | import os + | ^^ + | + help: Remove unused import: `os` + + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + Ok(()) +} + #[test] fn stdin_fix_when_not_fixable_should_still_print_contents() { let mut cmd = RuffCheck::default().args(["--fix"]).build(); diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 48a580e50961d..dbf31290d17c7 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -565,10 +565,7 @@ impl Configuration { }) .collect() }), - // `--extension` is a hidden command-line argument that isn't supported in configuration - // files at present. - extension: None, - + extension: options.extension.map(ExtensionMapping::from), lint: LintConfiguration::from_options(lint, project_root)?, format: FormatConfiguration::from_options( options.format.unwrap_or_default(), diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 07011ad66026c..78e35073a1d04 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -32,7 +32,7 @@ use ruff_linter::rules::{ pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, }; use ruff_linter::settings::types::{ - IdentifierPattern, OutputFormat, PythonVersion, RequiredVersion, + IdentifierPattern, Language, OutputFormat, PythonVersion, RequiredVersion, }; use ruff_linter::{RuleSelector, warn_user_once}; use ruff_macros::{CombineOptions, OptionsMetadata}; @@ -282,6 +282,20 @@ pub struct Options { )] pub respect_gitignore: Option, + /// A mapping of custom file extensions to known file types (overridden + /// by the `--extension` command-line flag). + /// + /// Supported file types include `python`, `pyi`, `ipynb`, and `markdown`. + #[option( + default = "{}", + value_type = "dict[str, Language]", + example = r#" + # Add a custom file extension mapped to Python + extension = {rpy="python"} + "# + )] + pub extension: Option>, + // Generic python options /// A list of builtins to treat as defined references, in addition to the /// system builtins. diff --git a/ruff.schema.json b/ruff.schema.json index 1de42c71a9ba7..8d95d780644f4 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -177,6 +177,16 @@ "$ref": "#/definitions/RuleSelector" } }, + "extension": { + "description": "A mapping of custom file extensions to known file types (overridden\nby the `--extension` command-line flag).\n\nSupported file types include `python`, `pyi`, `ipynb`, and `markdown`.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/Language" + } + }, "external": { "description": "A list of rule codes or prefixes that are unsupported by Ruff, but should be\npreserved when (e.g.) validating `# noqa` directives. Useful for\nretaining `# noqa` directives that cover plugins not yet implemented\nby Ruff.", "type": [ @@ -1976,6 +1986,15 @@ }, "additionalProperties": false }, + "Language": { + "type": "string", + "enum": [ + "python", + "pyi", + "ipynb", + "markdown" + ] + }, "LineEnding": { "oneOf": [ { From 7cc15f024b931fe56365f40de3fab01219c092c4 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Feb 2026 15:04:36 -0600 Subject: [PATCH 011/261] Bump 0.15.2 (#23430) --- CHANGELOG.md | 96 +++++++++++++++++++++++++++++++ Cargo.lock | 6 +- README.md | 6 +- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/formatter.md | 2 +- docs/integrations.md | 8 +-- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 11 files changed, 113 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d409471f0f105..0a9aa66538584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,101 @@ # Changelog +## 0.15.2 + +Released on 2026-02-19. + +### Preview features + +- Expand the default rule set ([#23385](https://github.com/astral-sh/ruff/pull/23385)) + + In preview, Ruff now enables a significantly expanded default rule set of 412 + rules, up from the stable default set of 59 rules. The new rules are mostly a + superset of the stable defaults, with the exception of these rules, which are + removed from the preview defaults: + + - [`multiple-imports-on-one-line`](https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line) (`E401`) + - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E402`) + - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E701`) + - [`multiple-statements-on-one-line-semicolon`](https://docs.astral.sh/ruff/rules/multiple-statements-on-one-line-semicolon) (`E702`) + - [`useless-semicolon`](https://docs.astral.sh/ruff/rules/useless-semicolon) (`E703`) + - [`none-comparison`](https://docs.astral.sh/ruff/rules/none-comparison) (`E711`) + - [`true-false-comparison`](https://docs.astral.sh/ruff/rules/true-false-comparison) (`E712`) + - [`not-in-test`](https://docs.astral.sh/ruff/rules/not-in-test) (`E713`) + - [`not-is-test`](https://docs.astral.sh/ruff/rules/not-is-test) (`E714`) + - [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison) (`E721`) + - [`lambda-assignment`](https://docs.astral.sh/ruff/rules/lambda-assignment) (`E731`) + - [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name) (`E741`) + - [`ambiguous-class-name`](https://docs.astral.sh/ruff/rules/ambiguous-class-name) (`E742`) + - [`ambiguous-function-name`](https://docs.astral.sh/ruff/rules/ambiguous-function-name) (`E743`) + - [`undefined-local-with-import-star`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star) (`F403`) + - [`undefined-local-with-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage) (`F405`) + - [`undefined-local-with-nested-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-nested-import-star-usage) (`F406`) + - [`forward-annotation-syntax-error`](https://docs.astral.sh/ruff/rules/forward-annotation-syntax-error) (`F722`) + + If you use preview and prefer the old defaults, you can restore them with + configuration like: + + ```toml + + # ruff.toml + + [lint] + select = ["E4", "E7", "E9", "F"] + + # pyproject.toml + + [tool.ruff.lint] + select = ["E4", "E7", "E9", "F"] + ``` + + If you do give them a try, feel free to share your feedback in the [GitHub + discussion](https://github.com/astral-sh/ruff/discussions/23203)! + +- \[`flake8-pyi`\] Also check string annotations (`PYI041`) ([#19023](https://github.com/astral-sh/ruff/pull/19023)) + +### Bug fixes + +- \[`flake8-async`\] Fix `in_async_context` logic ([#23426](https://github.com/astral-sh/ruff/pull/23426)) +- \[`ruff`\] Fix for `RUF102` should delete entire comment ([#23380](https://github.com/astral-sh/ruff/pull/23380)) +- \[`ruff`\] Suppress diagnostic for strings with backslashes in interpolations before Python 3.12 (`RUF027`) ([#21069](https://github.com/astral-sh/ruff/pull/21069)) +- \[`flake8-bugbear`\] Fix `B023` false positive for immediately-invoked lambdas ([#23294](https://github.com/astral-sh/ruff/pull/23294)) +- [parser] Fix false syntax error for match-like annotated assignments ([#23297](https://github.com/astral-sh/ruff/pull/23297)) +- [parser] Fix indentation tracking after line continuations ([#23417](https://github.com/astral-sh/ruff/pull/23417)) + +### Rule changes + +- \[`flake8-executable`\] Allow global flags in uv shebangs (`EXE003`) ([#22582](https://github.com/astral-sh/ruff/pull/22582)) +- \[`pyupgrade`\] Fix handling of `typing.{io,re}` (`UP035`) ([#23131](https://github.com/astral-sh/ruff/pull/23131)) +- \[`ruff`\] Detect `PLC0207` on chained `str.split()` calls ([#23275](https://github.com/astral-sh/ruff/pull/23275)) + +### CLI + +- Remove invalid inline `noqa` warning ([#23270](https://github.com/astral-sh/ruff/pull/23270)) + +### Configuration + +- Add extension mapping to configuration file options ([#23384](https://github.com/astral-sh/ruff/pull/23384)) + +### Documentation + +- Add `Q004` to the list of conflicting rules ([#23340](https://github.com/astral-sh/ruff/pull/23340)) +- \[`ruff`\] Expand `lint.external` docs and add sub-diagnostic (`RUF100`, `RUF102`) ([#23268](https://github.com/astral-sh/ruff/pull/23268)) + +### Contributors + +- [@dylwil3](https://github.com/dylwil3) +- [@Jkhall81](https://github.com/Jkhall81) +- [@danparizher](https://github.com/danparizher) +- [@dhruvmanila](https://github.com/dhruvmanila) +- [@harupy](https://github.com/harupy) +- [@ngnpope](https://github.com/ngnpope) +- [@amyreese](https://github.com/amyreese) +- [@kar-ganap](https://github.com/kar-ganap) +- [@robsdedude](https://github.com/robsdedude) +- [@shaanmajid](https://github.com/shaanmajid) +- [@ntBre](https://github.com/ntBre) +- [@toslunar](https://github.com/toslunar) + ## 0.15.1 Released on 2026-02-12. diff --git a/Cargo.lock b/Cargo.lock index e47e3df0419e4..6d803fcf9b978 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2990,7 +2990,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.15.1" +version = "0.15.2" dependencies = [ "anyhow", "argfile", @@ -3253,7 +3253,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.1" +version = "0.15.2" dependencies = [ "aho-corasick", "anyhow", @@ -3627,7 +3627,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.15.1" +version = "0.15.2" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index 1ab3c9d42652d..01512ea4315f6 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.15.1/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.15.1/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.15.2/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.15.2/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -186,7 +186,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 6f6bc7be2011d..8320cb989f884 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.15.1" +version = "0.15.2" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 40a77d868bef3..6b6dd89200a2a 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.15.1" +version = "0.15.2" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 61225b50d066e..5a67f2ee8d2b8 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.15.1" +version = "0.15.2" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index 145dd1af77e2f..d755f3b57990e 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -328,7 +328,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.1 + rev: v0.15.2 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/integrations.md b/docs/integrations.md index a03d559da13d1..04685ca84b44e 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.15.1-alpine + name: ghcr.io/astral-sh/ruff:0.15.2-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff-check @@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff-check @@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 7cdf536223bc7..9f5ecab1b9175 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 5d90046005f9b..e266125471b41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.15.1" +version = "0.15.2" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 366479c6e624a..5f7f9a1919700 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.15.1" +version = "0.15.2" description = "" authors = ["Charles Marsh "] From 9d18ee9115f9cbb4c21478baa7c1fa2b46e0759c Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Feb 2026 16:15:51 -0600 Subject: [PATCH 012/261] Hard code workflow name and `cancel-in-progress` only for PRs (#23431) This changes the names of concurrency groups in most of our workflows to use their hard-coded names instead of the name of the workflow that triggered them (e.g. `build-wasm-...` and `build-binaries-...` instead of `Release -...` for both). Hopefully this will reduce the number of times the jobs butt heads. I did not make this change for the CI workflow or for the Daily Fuzz workflow since it didn't seem relevant for those, but let me know if I should. --- .github/workflows/build-binaries.yml | 4 ++-- .github/workflows/build-wasm.yml | 4 ++-- .github/workflows/memory_report.yaml | 4 ++-- .github/workflows/mypy_primer.yaml | 4 ++-- .github/workflows/publish-ty-playground.yml | 4 ++-- .github/workflows/ty-ecosystem-analyzer.yaml | 4 ++-- .github/workflows/typing_conformance.yaml | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 5ad43e14875c9..176a37b2085b4 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -20,8 +20,8 @@ on: - .github/workflows/build-binaries.yml concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: build-binaries-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: {} diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index a866ab92a08d0..603e61e7e80ae 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -15,8 +15,8 @@ on: - .github/workflows/build-wasm.yml concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: build-wasm-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: {} diff --git a/.github/workflows/memory_report.yaml b/.github/workflows/memory_report.yaml index b34ecc5bb0743..34263d94a8451 100644 --- a/.github/workflows/memory_report.yaml +++ b/.github/workflows/memory_report.yaml @@ -25,8 +25,8 @@ on: - "!crates/ty_python_semantic/resources/corpus/**" concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true + group: memory-report-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: CARGO_INCREMENTAL: 0 diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 3af62f10e8ab4..cf3b9ab115c82 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -25,8 +25,8 @@ on: - "!crates/ty_python_semantic/resources/corpus/**" concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true + group: mypy-primer-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} defaults: run: diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml index a7828c18c9486..c569f771c11e9 100644 --- a/.github/workflows/publish-ty-playground.yml +++ b/.github/workflows/publish-ty-playground.yml @@ -15,8 +15,8 @@ on: - ".github/workflows/publish-ty-playground.yml" concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }} - cancel-in-progress: true + group: publish-ty-playground-${{ github.ref_name }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: CARGO_INCREMENTAL: 0 diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index c7d2ee5dd08fb..6935274562afd 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -13,8 +13,8 @@ on: - reopened concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true + group: ty-ecosystem-analyzer-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: CARGO_INCREMENTAL: 0 diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index 40617075c80d9..a302d1ca933d7 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -25,8 +25,8 @@ on: - "!crates/ty_python_semantic/resources/corpus/**" concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true + group: typing-conformance-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: CARGO_INCREMENTAL: 0 From 333ad91ba3011be20a181d614ab61da664537988 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Fri, 20 Feb 2026 00:32:01 +0100 Subject: [PATCH 013/261] [`pylint`] Implement `swap-with-temporary-variable` (`PLR1712`) (#22205) ## Summary This PR implements the `consider-swap-variables` rule from pylint. Basically it tries to find code parts that swap two variables with each other using a temporary variable. Example code: ```py temp = x x = y y = temp ``` can be simplified to ```py x, y = y, x ``` related: - https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/consider-swap-variables.html - #970 ## Test Plan I've added new snapshots tests. PS: Since this is my first contribution here and I'm not too familiar with the codebase, suggestions are very welcome! The implementation might also not be 100% memory-optimized yet, since we use `clone` a few times. --- .../pylint/swap_with_temporary_variable.py | 62 +++++ .../checkers/ast/analyze/deferred_scopes.rs | 5 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/pylint/mod.rs | 4 + .../ruff_linter/src/rules/pylint/rules/mod.rs | 2 + .../rules/swap_with_temporary_variable.rs | 224 ++++++++++++++++++ ...R1712_swap_with_temporary_variable.py.snap | 68 ++++++ ruff.schema.json | 1 + 8 files changed, 367 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pylint/swap_with_temporary_variable.py create mode 100644 crates/ruff_linter/src/rules/pylint/rules/swap_with_temporary_variable.rs create mode 100644 crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/swap_with_temporary_variable.py b/crates/ruff_linter/resources/test/fixtures/pylint/swap_with_temporary_variable.py new file mode 100644 index 0000000000000..54e2dc330d6d7 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/swap_with_temporary_variable.py @@ -0,0 +1,62 @@ +# safe fix +def foo(x: int, y: int): + temp: int = x + x = y + y = temp + + +# safe fix inside an if condition +def foo(x: int, y: int): + if x > 5: + temp: int = x + x = y + y = temp + + +# unsafe fix because the swap statements contain a comment +def bar(x: int, y: int): + temp: int = x # comment + x = y + y = temp + + +# not a swap statement +def baz(x: int, y: int): + temp = x + x = y + y = x + + +# not a simple swap statement because temp variable is re-used later +def foobar(x: int, y: int): + temp = x + x = y + y = temp + + # use temp variable again, + # so its declaration can't be removed + z = temp + + +# not a simple swap statement because the temp variable is global +swap_var = 0 + + +def quux(x: int, y: int): + global swap_var + swap_var = x + x = y + y = swap_var + + +# temp is read somewhere else in the code, so this is not a simple swap statement and hence ignored +def foo(x, y): + temp = [] + + def bar(): + print(temp) + + temp = x + x = y + y = temp + bar() diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs index 26cf91873165d..81dee25d3a183 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs @@ -28,6 +28,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) { Rule::RuntimeImportInTypeCheckingBlock, Rule::SingledispatchMethod, Rule::SingledispatchmethodFunction, + Rule::SwapWithTemporaryVariable, Rule::TooManyLocals, Rule::TypingOnlyFirstPartyImport, Rule::TypingOnlyStandardLibraryImport, @@ -94,6 +95,10 @@ pub(crate) fn deferred_scopes(checker: &Checker) { pylint::rules::global_variable_not_assigned(checker, scope); } + if checker.is_rule_enabled(Rule::SwapWithTemporaryVariable) { + pylint::rules::swap_with_temporary_variable(checker, scope_id, scope); + } + if checker.is_rule_enabled(Rule::RedefinedArgumentFromLocal) { pylint::rules::redefined_argument_from_local(checker, scope_id, scope); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 25096b73aaf51..28a7f01da1eb4 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -288,6 +288,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R1706") => rules::pylint::rules::AndOrTernary, (Pylint, "R1708") => rules::pylint::rules::StopIterationReturn, (Pylint, "R1711") => rules::pylint::rules::UselessReturn, + (Pylint, "R1712") => rules::pylint::rules::SwapWithTemporaryVariable, (Pylint, "R1714") => rules::pylint::rules::RepeatedEqualityComparison, (Pylint, "R1722") => rules::pylint::rules::SysExitAlias, (Pylint, "R1730") => rules::pylint::rules::IfStmtMinMax, diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index de341b1146ba2..c8ae94c246f37 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -46,6 +46,10 @@ mod tests { #[test_case(Rule::CompareToEmptyString, Path::new("compare_to_empty_string.py"))] #[test_case(Rule::ComparisonOfConstant, Path::new("comparison_of_constant.py"))] #[test_case(Rule::ComparisonWithItself, Path::new("comparison_with_itself.py"))] + #[test_case( + Rule::SwapWithTemporaryVariable, + Path::new("swap_with_temporary_variable.py") + )] #[test_case(Rule::EqWithoutHash, Path::new("eq_without_hash.py"))] #[test_case(Rule::EmptyComment, Path::new("empty_comment.py"))] #[test_case(Rule::EmptyComment, Path::new("empty_comment_line_continuation.py"))] diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index 2853bb84c9dc7..070ad5797ffbc 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -79,6 +79,7 @@ pub(crate) use stop_iteration_return::*; pub(crate) use subprocess_popen_preexec_fn::*; pub(crate) use subprocess_run_without_check::*; pub(crate) use super_without_brackets::*; +pub(crate) use swap_with_temporary_variable::*; pub(crate) use sys_exit_alias::*; pub(crate) use too_many_arguments::*; pub(crate) use too_many_boolean_expressions::*; @@ -190,6 +191,7 @@ mod stop_iteration_return; mod subprocess_popen_preexec_fn; mod subprocess_run_without_check; mod super_without_brackets; +mod swap_with_temporary_variable; mod sys_exit_alias; mod too_many_arguments; mod too_many_boolean_expressions; diff --git a/crates/ruff_linter/src/rules/pylint/rules/swap_with_temporary_variable.rs b/crates/ruff_linter/src/rules/pylint/rules/swap_with_temporary_variable.rs new file mode 100644 index 0000000000000..01df57ac71b2b --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/swap_with_temporary_variable.rs @@ -0,0 +1,224 @@ +use ruff_diagnostics::{Applicability, Edit, Fix}; +use ruff_python_ast::name::Name; +use ruff_python_ast::{Stmt, traversal}; +use ruff_python_semantic::{BindingId, Scope, ScopeId, SemanticModel}; +use ruff_text_size::{Ranged, TextRange}; + +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for code that swaps two variables using a temporary variable. +/// +/// ## Why is this bad? +/// Variables can be swapped by using tuple unpacking instead of using a +/// temporary variable. That also makes the intention of the swapping logic +/// more clear. +/// +/// ## Example +/// ```python +/// def function(x, y): +/// if x > y: +/// temp = x +/// x = y +/// y = temp +/// assert x <= y +/// ``` +/// +/// Use instead: +/// ```python +/// def function(x, y): +/// if x > y: +/// x, y = y, x +/// assert x <= y +/// ``` +/// +/// ## Fix safety +/// The rule's fix is marked as safe, unless the replacement range contains comments +/// that would be removed. +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] +pub(crate) struct SwapWithTemporaryVariable<'a> { + first: &'a Name, + second: &'a Name, +} + +impl Violation for SwapWithTemporaryVariable<'_> { + const FIX_AVAILABILITY: crate::FixAvailability = crate::FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + "Unnecessary temporary variable".to_string() + } + + fn fix_title(&self) -> Option { + let SwapWithTemporaryVariable { first, second } = self; + + Some(format!( + "Use `{first}, {second} = {second}, {first}` instead" + )) + } +} + +pub(crate) fn swap_with_temporary_variable(checker: &Checker, scope_id: ScopeId, scope: &Scope) { + let consecutive_assignments = scope.binding_ids().filter_map(|binding_id| { + match_consecutive_assignments(checker.semantic(), scope_id, binding_id) + }); + + for (stmt_a, stmt_b, stmt_c) in consecutive_assignments { + // Detect patterns like: + // temp = x + // x = y + // y = temp + if stmt_a.value == stmt_b.target + && stmt_b.value == stmt_c.target + && stmt_a.target == stmt_c.value + { + // check whether there is any later read reference to the temporary variable - + // in this case the automatic hotfix would result in broken code, because + // this later read would attempt to read from a variable that no longer exists + let is_variable_reused_later = is_variable_read_after(checker, &stmt_a); + if is_variable_reused_later { + continue; + } + + let first = stmt_b.target; + let second = stmt_c.target; + let edit_range = TextRange::new(stmt_a.start(), stmt_c.end()); + let edit = Edit::range_replacement( + format!("{first}, {second} = {second}, {first}"), + edit_range, + ); + + // The quick fix would remove comments, hence it's unsafe if there are any comments in the relevant code part. + let applicability = if checker.comment_ranges().intersects(edit.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + + checker + .report_diagnostic(SwapWithTemporaryVariable { first, second }, edit_range) + .set_fix(Fix::applicable_edit(edit, applicability)); + } + } +} + +/// Match consecutive assignment statements. +/// +/// Also see the `repeated_append` rule for a similar use case. +fn match_consecutive_assignments<'a>( + semantic: &'a SemanticModel<'a>, + scope_id: ScopeId, + binding_id: BindingId, +) -> Option<( + VarToVarAssignment<'a>, + VarToVarAssignment<'a>, + VarToVarAssignment<'a>, +)> { + let binding = semantic.binding(binding_id); + + // Only consider simple assignments (no imports, function defs, etc.) + if !binding.kind.is_assignment() { + return None; + } + + let node_id = binding.source?; + + let stmt = binding.statement(semantic)?; + let stmt_a = VarToVarAssignment::from_stmt(stmt)?; + + // Find the enclosing suite so we can look at the next siblings. + // For the global scope, use the module body; otherwise, find the parent statement. + let suite = if scope_id.is_global() { + traversal::EnclosingSuite::new(semantic.definitions.python_ast()?, stmt.into()) + } else { + traversal::suite(stmt, semantic.parent_statement(node_id)?) + }?; + + let stmt_b = VarToVarAssignment::from_stmt(suite.next_sibling()?)?; + let stmt_c = VarToVarAssignment::from_stmt(suite.next_siblings().get(1)?)?; + + Some((stmt_a, stmt_b, stmt_c)) +} + +#[derive(Eq, PartialEq, Debug, Clone)] +struct VarToVarAssignment<'a> { + target: &'a Name, + value: &'a Name, + range: TextRange, +} + +impl Ranged for VarToVarAssignment<'_> { + fn range(&self) -> TextRange { + self.range + } +} + +impl<'a> VarToVarAssignment<'a> { + fn from_stmt(stmt: &'a Stmt) -> Option> { + let (target, value) = match stmt { + Stmt::Assign(stmt_assign) => { + // only one variable is expected for matching the pattern + let [target_variable] = stmt_assign.targets.as_slice() else { + return None; + }; + + (target_variable, &stmt_assign.value) + } + Stmt::AnnAssign(stmt_ann_assign) => { + // only assignments that actually assign a value are relevant here + let Some(value) = &stmt_ann_assign.value else { + return None; + }; + + (&*stmt_ann_assign.target, value) + } + // Stmt::AugAssign is not relevant because it modifies the content + // of a variable based on its existing value, so it can't swap variables + _ => return None, + }; + + // assignment value is more complex than just a simple variable, skip such cases. + if let (Some(target_expr), Some(value_expr)) = (target.as_name_expr(), value.as_name_expr()) + { + Some(Self { + target: &target_expr.id, + value: &value_expr.id, + range: stmt.range(), + }) + } else { + None + } + } +} + +/// Check whether a variable is read after a given position. +/// +/// Returns `true` if the variable assigned to in `variable_assignment` is read anywhere other than the swap statement. +fn is_variable_read_after(checker: &Checker, variable_assignment: &VarToVarAssignment) -> bool { + // Get the variable binding for the variable assigned to in this statement, + // e.g., in the example `a = b` this would be the binding to the variable `a`. + let Some(variable_binding) = checker + .semantic() + .bindings + .iter() + .find(|binding| variable_assignment.range.contains_range(binding.range)) + else { + return true; + }; + + // If the variable is global (e.g., `global VARNAME`) or nonlocal (e.g., `nonlocal VARNAME`), + // then it is intended to also be used elsewhere outside our scope and hence it's likely + // to be used in other contexts as well. + if variable_binding.is_global() || variable_binding.is_nonlocal() { + return true; + } + + // Check if there's any read reference to the variable other than the one from the swap statement + // We already confirmed that there is at least one reference (i.e. `y = temp`), so the variable is + // only re-used if there is any other reference than this one (i.e. reference count > 1). + variable_binding.references().count() > 1 +} diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap new file mode 100644 index 0000000000000..943005a442499 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +PLR1712 [*] Unnecessary temporary variable + --> swap_with_temporary_variable.py:3:5 + | +1 | # safe fix +2 | def foo(x: int, y: int): +3 | / temp: int = x +4 | | x = y +5 | | y = temp + | |____________^ + | +help: Use `x, y = y, x` instead +1 | # safe fix +2 | def foo(x: int, y: int): + - temp: int = x + - x = y + - y = temp +3 + x, y = y, x +4 | +5 | +6 | # safe fix inside an if condition + +PLR1712 [*] Unnecessary temporary variable + --> swap_with_temporary_variable.py:11:9 + | + 9 | def foo(x: int, y: int): +10 | if x > 5: +11 | / temp: int = x +12 | | x = y +13 | | y = temp + | |________________^ + | +help: Use `x, y = y, x` instead +8 | # safe fix inside an if condition +9 | def foo(x: int, y: int): +10 | if x > 5: + - temp: int = x + - x = y + - y = temp +11 + x, y = y, x +12 | +13 | +14 | # unsafe fix because the swap statements contain a comment + +PLR1712 [*] Unnecessary temporary variable + --> swap_with_temporary_variable.py:18:5 + | +16 | # unsafe fix because the swap statements contain a comment +17 | def bar(x: int, y: int): +18 | / temp: int = x # comment +19 | | x = y +20 | | y = temp + | |____________^ + | +help: Use `x, y = y, x` instead +15 | +16 | # unsafe fix because the swap statements contain a comment +17 | def bar(x: int, y: int): + - temp: int = x # comment + - x = y + - y = temp +18 + x, y = y, x +19 | +20 | +21 | # not a swap statement +note: This is an unsafe fix and may change runtime behavior diff --git a/ruff.schema.json b/ruff.schema.json index 8d95d780644f4..8a211304a4b1d 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3815,6 +3815,7 @@ "PLR1708", "PLR171", "PLR1711", + "PLR1712", "PLR1714", "PLR1716", "PLR172", From d537f03d59873e85f4e6498a731030bef21001dd Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 20 Feb 2026 07:18:57 -0600 Subject: [PATCH 014/261] Update the Python module (notably `find_ruff_bin`) for parity with uv (#23406) Closes https://github.com/astral-sh/uv/issues/14874 Closes https://github.com/astral-sh/ruff/issues/23402 uv has fairly extensive test coverage for this functionality but it seems challenging to copy it over My smoke test strategy was to ask an LLM to build the wheel and test all of the cases ``` $ uv build --wheel Building wheel... Successfully built dist/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl $ WHEEL=dist/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl $ uv venv -q .smoke-venv && uv pip install -q --python .smoke-venv $WHEEL $ .smoke-venv/bin/python -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/workspace/ruff/.smoke-venv/bin/ruff $ .smoke-venv/bin/python -m ruff version ruff 0.15.1+81 (1e42d4f11 2026-02-18) $ uv run --no-project --with $WHEEL -- python -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/.cache/uv/archive-v0/zf7_vNji2jmEGEDox-9Vj/bin/ruff $ uv run --no-project --with $WHEEL -- python -m ruff version ruff 0.15.1+81 (1e42d4f11 2026-02-18) $ uv pip install --target .smoke-target $WHEEL $ PYTHONPATH=.smoke-target python3 -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/workspace/ruff/.smoke-target/bin/ruff $ uv pip install --prefix .smoke-prefix $WHEEL $ PYTHONPATH=.smoke-prefix/lib/python3.14/site-packages python3 -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/workspace/ruff/.smoke-prefix/bin/ruff $ python3 -m pip install --user --break-system-packages $WHEEL $ python3 -c "from ruff import find_ruff_bin; print(find_ruff_bin())" /Users/zb/Library/Python/3.13/bin/ruff $ python3 -m ruff version ruff 0.15.1+81 (1e42d4f11 2026-02-18) ``` --- python/ruff/__init__.py | 5 ++ python/ruff/__main__.py | 87 +++++-------------------------- python/ruff/_find_ruff.py | 104 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 74 deletions(-) create mode 100644 python/ruff/_find_ruff.py diff --git a/python/ruff/__init__.py b/python/ruff/__init__.py index e69de29bb2d1d..07f41420d184b 100644 --- a/python/ruff/__init__.py +++ b/python/ruff/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from ._find_ruff import find_ruff_bin + +__all__ = ["find_ruff_bin"] diff --git a/python/ruff/__main__.py b/python/ruff/__main__.py index 8569844c4ed34..875131923e2f6 100644 --- a/python/ruff/__main__.py +++ b/python/ruff/__main__.py @@ -2,87 +2,26 @@ import os import sys -import sysconfig +from ruff import find_ruff_bin -def find_ruff_bin() -> str: - """Return the ruff binary path.""" - ruff_exe = "ruff" + sysconfig.get_config_var("EXE") - - scripts_path = os.path.join(sysconfig.get_path("scripts"), ruff_exe) - if os.path.isfile(scripts_path): - return scripts_path - - if sys.version_info >= (3, 10): - user_scheme = sysconfig.get_preferred_scheme("user") - elif os.name == "nt": - user_scheme = "nt_user" - elif sys.platform == "darwin" and sys._framework: - user_scheme = "osx_framework_user" - else: - user_scheme = "posix_user" - - user_path = os.path.join( - sysconfig.get_path("scripts", scheme=user_scheme), ruff_exe - ) - if os.path.isfile(user_path): - return user_path - - # Search in `bin` adjacent to package root (as created by `pip install --target`). - pkg_root = os.path.dirname(os.path.dirname(__file__)) - target_path = os.path.join(pkg_root, "bin", ruff_exe) - if os.path.isfile(target_path): - return target_path - - # Search for pip-specific build environments. - # - # Expect to find ruff in /pip-build-env-/overlay/bin/ruff - # Expect to find a "normal" folder at /pip-build-env-/normal - # - # See: https://github.com/pypa/pip/blob/102d8187a1f5a4cd5de7a549fd8a9af34e89a54f/src/pip/_internal/build_env.py#L87 - paths = os.environ.get("PATH", "").split(os.pathsep) - if len(paths) >= 2: - - def get_last_three_path_parts(path: str) -> list[str]: - """Return a list of up to the last three parts of a path.""" - parts = [] - - while len(parts) < 3: - head, tail = os.path.split(path) - if tail or head != path: - parts.append(tail) - path = head - else: - parts.append(path) - break - - return parts - - maybe_overlay = get_last_three_path_parts(paths[0]) - maybe_normal = get_last_three_path_parts(paths[1]) - if ( - len(maybe_normal) >= 3 - and maybe_normal[-1].startswith("pip-build-env-") - and maybe_normal[-2] == "normal" - and len(maybe_overlay) >= 3 - and maybe_overlay[-1].startswith("pip-build-env-") - and maybe_overlay[-2] == "overlay" - ): - # The overlay must contain the ruff binary. - candidate = os.path.join(paths[0], ruff_exe) - if os.path.isfile(candidate): - return candidate - - raise FileNotFoundError(scripts_path) - - -if __name__ == "__main__": +def _run() -> None: ruff = find_ruff_bin() + if sys.platform == "win32": import subprocess - completed_process = subprocess.run([ruff, *sys.argv[1:]]) + # Avoid emitting a traceback on interrupt + try: + completed_process = subprocess.run([ruff, *sys.argv[1:]]) + except KeyboardInterrupt: + sys.exit(2) + sys.exit(completed_process.returncode) else: os.execvp(ruff, [ruff, *sys.argv[1:]]) + + +if __name__ == "__main__": + _run() diff --git a/python/ruff/_find_ruff.py b/python/ruff/_find_ruff.py new file mode 100644 index 0000000000000..c0213bb23fde2 --- /dev/null +++ b/python/ruff/_find_ruff.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import os +import sys +import sysconfig + + +class RuffNotFound(FileNotFoundError): ... + + +def find_ruff_bin() -> str: + """Return the ruff binary path.""" + + ruff_exe = "ruff" + sysconfig.get_config_var("EXE") + + targets = [ + # The scripts directory for the current Python + sysconfig.get_path("scripts"), + # The scripts directory for the base prefix + sysconfig.get_path("scripts", vars={"base": sys.base_prefix}), + # Above the package root, e.g., from `pip install --prefix` or `uv run --with` + ( + # On Windows, with module path `/Lib/site-packages/ruff` + _join( + _matching_parents(_module_path(), "Lib/site-packages/ruff"), "Scripts" + ) + if sys.platform == "win32" + # On Unix, with module path `/lib/python3.13/site-packages/ruff` + else _join( + _matching_parents(_module_path(), "lib/python*/site-packages/ruff"), + "bin", + ) + ), + # Adjacent to the package root, e.g., from `pip install --target` + # with module path `/ruff` + _join(_matching_parents(_module_path(), "ruff"), "bin"), + # The user scheme scripts directory, e.g., `~/.local/bin` + sysconfig.get_path("scripts", scheme=_user_scheme()), + ] + + seen = [] + for target in targets: + if not target: + continue + if target in seen: + continue + seen.append(target) + path = os.path.join(target, ruff_exe) + if os.path.isfile(path): + return path + + locations = "\n".join(f" - {target}" for target in seen) + raise RuffNotFound( + f"Could not find the ruff binary in any of the following locations:\n{locations}\n" + ) + + +def _module_path() -> str | None: + path = os.path.dirname(__file__) + return path + + +def _matching_parents(path: str | None, match: str) -> str | None: + """ + Return the parent directory of `path` after trimming a `match` from the end. + The match is expected to contain `/` as a path separator, while the `path` + is expected to use the platform's path separator (e.g., `os.sep`). The path + components are compared case-insensitively and a `*` wildcard can be used + in the `match`. + """ + from fnmatch import fnmatch + + if not path: + return None + parts = path.split(os.sep) + match_parts = match.split("/") + if len(parts) < len(match_parts): + return None + + if not all( + fnmatch(part, match_part) + for part, match_part in zip(reversed(parts), reversed(match_parts)) + ): + return None + + return os.sep.join(parts[: -len(match_parts)]) + + +def _join(path: str | None, *parts: str) -> str | None: + if not path: + return None + return os.path.join(path, *parts) + + +def _user_scheme() -> str: + if sys.version_info >= (3, 10): + user_scheme = sysconfig.get_preferred_scheme("user") + elif os.name == "nt": + user_scheme = "nt_user" + elif sys.platform == "darwin" and sys._framework: # ty: ignore[unresolved-attribute] + user_scheme = "osx_framework_user" + else: + user_scheme = "posix_user" + return user_scheme From 735b5116a2a23107752d001b11b0994b41203578 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 20 Feb 2026 08:26:55 -0500 Subject: [PATCH 015/261] [ty] Fix incorrect types inferred when unpacking mixed tuples (#23437) ## Summary When unpacking a "mixed tuple" like `tuple[I0, *tuple[I1, ...], I2]` with a starred expression (e.g., `[a, b, *c] = x`), ty incorrectly inferred `I1` for `b` instead of `I1 | I2`. The variable-length part can materialize to 0 elements, causing `I2` to shift into the `b` position, so the correct type is the union `I1 | I2`. Closes: https://github.com/astral-sh/ty/issues/947. --- .../ty_python_semantic/resources/mdtest/unpacking.md | 12 +++--------- crates/ty_python_semantic/src/types/tuple.rs | 11 ++++++++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/unpacking.md b/crates/ty_python_semantic/resources/mdtest/unpacking.md index b8889f93ccca6..31116647e0334 100644 --- a/crates/ty_python_semantic/resources/mdtest/unpacking.md +++ b/crates/ty_python_semantic/resources/mdtest/unpacking.md @@ -489,19 +489,13 @@ def f(x: MixedTupleSubclass): [n, o, *p] = x reveal_type(n) # revealed: I0 - - # TODO: `I1 | I2` might be better here? (https://github.com/astral-sh/ty/issues/947) - reveal_type(o) # revealed: I1 - + reveal_type(o) # revealed: I1 | I2 reveal_type(p) # revealed: list[I1 | I2] [o, p, q, *r] = x reveal_type(o) # revealed: I0 - - # TODO: `I1 | I2` might be better for both of these? (https://github.com/astral-sh/ty/issues/947) - reveal_type(p) # revealed: I1 - reveal_type(q) # revealed: I1 - + reveal_type(p) # revealed: I1 | I2 + reveal_type(q) # revealed: I1 | I2 reveal_type(r) # revealed: list[I1 | I2] s, *t, u = x diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 1ed94269d1d4f..b32d4009ec89f 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -903,8 +903,11 @@ impl<'db> VariableLengthTuple> { let self_suffix_length = self.suffix_elements().len(); let suffix_overflow = self_suffix_length.saturating_sub(suffix_length); let suffix_underflow = suffix_length.saturating_sub(self_suffix_length); - let prefix = (self.iter_prefix_elements().take(prefix_length)) - .chain(std::iter::repeat_n(self.variable(), prefix_underflow)); + // Compute the variable element first, since underflow positions can + // receive any element that could appear in the variable portion. + // For example, `tuple[I0, *tuple[I1, ...], I2]` unpacked as + // `[a, b, *c]` means `b` could be `I1` (variable non-empty) or + // `I2` (variable empty, suffix shifts left), so it should be `I1 | I2`. let variable = UnionType::from_elements_leave_aliases( db, self.iter_prefix_elements() @@ -912,7 +915,9 @@ impl<'db> VariableLengthTuple> { .chain(std::iter::once(self.variable())) .chain(self.iter_suffix_elements().take(suffix_overflow)), ); - let suffix = std::iter::repeat_n(self.variable(), suffix_underflow) + let prefix = (self.iter_prefix_elements().take(prefix_length)) + .chain(std::iter::repeat_n(variable, prefix_underflow)); + let suffix = std::iter::repeat_n(variable, suffix_underflow) .chain(self.iter_suffix_elements().skip(suffix_overflow)); Ok(VariableLengthTuple::mixed(prefix, variable, suffix)) } From 3ffd763f6bc9318c48a49bc652c1943a9e8ac768 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 20 Feb 2026 14:32:38 +0100 Subject: [PATCH 016/261] [ty] Flag illegal special form types in PEP 613 type aliases (#23444) ## Summary What it says in the title ## Test Plan New Markdown tests --- .../resources/mdtest/pep613_type_aliases.md | 24 +++++++++ .../src/types/infer/builder.rs | 25 +++++++++ .../src/types/special_form.rs | 53 +++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index 3c1f8831499e0..ad901d4042c28 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -460,3 +460,27 @@ BadTypeAlias14: TypeAlias = Literal[-3.14] # error: [invalid-type-form] # error: [invalid-type-form] BadTypeAlias14: TypeAlias = Literal[3.14] ``` + +## No type qualifiers + +The right-hand side of a type alias definition is a [type expression], not an annotation expression. +Type qualifiers like `ClassVar` and `Final` are only valid in annotation expressions, so they cannot +appear in type alias definitions: + +```py +from typing_extensions import ClassVar, Final, Required, NotRequired, ReadOnly, TypeAlias, Unpack +from dataclasses import InitVar + +bad1: TypeAlias = ClassVar[str] # error: [invalid-type-form] +bad2: TypeAlias = ClassVar # error: [invalid-type-form] +bad3: TypeAlias = Final[int] # error: [invalid-type-form] +bad4: TypeAlias = Final # error: [invalid-type-form] +bad5: TypeAlias = Required[int] # error: [invalid-type-form] +bad6: TypeAlias = NotRequired[int] # error: [invalid-type-form] +bad7: TypeAlias = ReadOnly[int] # error: [invalid-type-form] +bad8: TypeAlias = Unpack[tuple[int, ...]] # error: [invalid-type-form] +bad9: TypeAlias = InitVar[int] # error: [invalid-type-form] +bad10: TypeAlias = InitVar # error: [invalid-type-form] +``` + +[type expression]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 36ff0f03012e3..c86636f546eae 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -9159,6 +9159,31 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if is_pep_613_type_alias { + let is_valid_special_form = |ty: Type<'db>| match ty { + Type::SpecialForm(special_form) => special_form.is_valid_in_type_expression(), + Type::ClassLiteral(literal) + if literal.is_known(self.db(), KnownClass::InitVar) => + { + false + } + _ => true, + }; + + let is_invalid = match value { + ast::Expr::Subscript(sub) => { + !is_valid_special_form(self.expression_type(&sub.value)) + } + _ => !is_valid_special_form(self.expression_type(value)), + }; + + if is_invalid + && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, value) + { + builder.into_diagnostic( + "Type qualifiers are not allowed in type alias definitions", + ); + } + let inferred_ty = if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = inferred_ty { let identity = TypeVarIdentity::new( diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index d78f110018a84..7d2d43f97dcdf 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -331,6 +331,59 @@ impl SpecialFormType { } } + /// Return `true` if this special form type is valid in a type-expression context (and not + /// just in an *annotation* expression context). See the following section of the typing + /// specification for more details: + /// + pub(super) const fn is_valid_in_type_expression(self) -> bool { + match self { + Self::ClassVar + | Self::Final + | Self::Required + | Self::NotRequired + | SpecialFormType::ReadOnly + | SpecialFormType::Unpack + | SpecialFormType::TypeAlias => false, + Self::Annotated + | SpecialFormType::Any + | SpecialFormType::Literal + | SpecialFormType::LiteralString + | SpecialFormType::Optional + | SpecialFormType::Union + | SpecialFormType::NoReturn + | SpecialFormType::Never + | SpecialFormType::Tuple + | SpecialFormType::List + | SpecialFormType::Dict + | SpecialFormType::Set + | SpecialFormType::FrozenSet + | SpecialFormType::ChainMap + | SpecialFormType::Counter + | SpecialFormType::DefaultDict + | SpecialFormType::Deque + | SpecialFormType::OrderedDict + | SpecialFormType::Type + | SpecialFormType::Unknown + | SpecialFormType::AlwaysTruthy + | SpecialFormType::AlwaysFalsy + | SpecialFormType::Not + | SpecialFormType::Intersection + | SpecialFormType::TypeOf + | SpecialFormType::CallableTypeOf + | SpecialFormType::Top + | SpecialFormType::Bottom + | SpecialFormType::Callable + | SpecialFormType::TypingSelf + | SpecialFormType::Concatenate + | SpecialFormType::TypeGuard + | SpecialFormType::TypedDict + | SpecialFormType::TypeIs + | SpecialFormType::Protocol + | SpecialFormType::Generic + | SpecialFormType::NamedTuple => true, + } + } + /// Return `Some(KnownClass)` if this special form is an alias /// to a standard library class. pub(super) const fn aliased_stdlib_class(self) -> Option { From 72a878c766576a34c5b9851be8327bd96105992d Mon Sep 17 00:00:00 2001 From: Jaap Roes Date: Fri, 20 Feb 2026 14:43:59 +0100 Subject: [PATCH 017/261] [`pydocstyle`] Fix double comma in description of `D404` (#23440) ## Summary Fix a small typo in the description of a rule ## Test Plan n/a --- .../ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs index d3e94659187e8..9d4617396bcb3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs @@ -18,7 +18,7 @@ use crate::rules::pydocstyle::helpers::normalize_word; /// /// This rule may not apply to all projects; its applicability is a matter of /// convention. By default, this rule is enabled when using the `numpy` -/// convention,, and disabled when using the `google` and `pep257` conventions. +/// convention, and disabled when using the `google` and `pep257` conventions. /// /// ## Example /// ```python From beda157528c5e7598f757b009c026f3e2f262cbe Mon Sep 17 00:00:00 2001 From: Anish Giri <161533316+anishgirianish@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:20:19 -0600 Subject: [PATCH 018/261] [`ruff`] Add `unnecessary-assign-before-yield` (`RUF070`) (#23300) ## Summary Closes #13141 Adds a new rule `unnecessary-assign-before-yield` (`RUF070`) that detects variable assignments immediately followed by a `yield` (or `yield from`) of that variable, where the variable is not referenced anywhere else. This is the `yield` equivalent of `RET504` (`unnecessary-assign`). ```python # Before def gen(): x = 1 yield x # After def gen(): yield 1 ``` Unlike return, yield does not exit the function, so the rule only triggers when the binding has exactly one reference (the yielditself). The fix is marked as unsafe for the same reason. ## Test Plan cargo nextest run -p ruff_linter -- RUF070 --------- Co-authored-by: Brent Westbrook --- .../resources/test/fixtures/ruff/RUF070.py | 195 ++++++++++++ .../src/checkers/ast/analyze/bindings.rs | 9 + crates/ruff_linter/src/codes.rs | 1 + .../src/rules/flake8_return/mod.rs | 2 + .../src/rules/flake8_return/visitor.rs | 2 +- crates/ruff_linter/src/rules/ruff/mod.rs | 1 + .../ruff_linter/src/rules/ruff/rules/mod.rs | 2 + .../rules/unnecessary_assign_before_yield.rs | 237 +++++++++++++++ ...uff__tests__preview__RUF070_RUF070.py.snap | 286 ++++++++++++++++++ ruff.schema.json | 2 + 10 files changed, 736 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF070.py create mode 100644 crates/ruff_linter/src/rules/ruff/rules/unnecessary_assign_before_yield.rs create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF070.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF070.py new file mode 100644 index 0000000000000..cfd134283aa77 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF070.py @@ -0,0 +1,195 @@ +### +# Errors +### + +def foo(): + x = 1 + yield x # RUF070 + +def foo(): + x = [1, 2, 3] + yield from x # RUF070 + +def foo(): + for i in range(10): + x = i * 2 + yield x # RUF070 + +def foo(): + if True: + x = 1 + yield x # RUF070 + +def foo(): + with open("foo.txt") as f: + x = f.read() + yield x # RUF070 + +def foo(): + try: + x = something() + yield x # RUF070 + except Exception: + pass + +def foo(): + x = some_function() + yield x # RUF070 + +def foo(): + x = some_generator() + yield from x # RUF070 + +def foo(): + x = lambda: 1 + yield x # RUF070 + +def foo(): + x = (y := 1) + yield x # RUF070 + +def foo(): + x =1 + yield x # RUF070 (no space after `=`) + +def foo(): + x = yield 1 + yield x # RUF070 (yield as assigned value, fix adds parentheses) + +# Assignment inside `with`, yield outside +def foo(): + with open("foo.txt") as f: + x = f.read() + yield x # RUF070 + + +### +# Non-errors +### + +# Variable used after yield +def foo(): + x = 1 + yield x + print(x) + +# Variable used before yield (two references) +def foo(): + x = 1 + print(x) + yield x + +# Multiple yields of same var +def foo(): + x = 1 + yield x + yield x + +# Annotated variable +def foo(): + x: int + x = 1 + yield x + +# Nonlocal variable +def foo(): + x = 0 + def bar(): + nonlocal x + x = 1 + yield x + +# Global variable +def foo(): + global x + x = 1 + yield x + +# Intervening statement between assign and yield +def foo(): + x = 1 + print("hello") + yield x + +# Augmented assignment +def foo(): + x = 1 + x += 1 + yield x + +# Unpacking assignment +def foo(): + x, y = 1, 2 + yield x + +# Non-name target (attribute) +def foo(): + self.x = 1 + yield self.x + +# Yield with no value +def foo(): + x = 1 + yield + +# Multiple assignment targets (e.g. `x = y = 1`) +def foo(): + x = y = 1 + yield x + +# Different variable names +def foo(): + x = 1 + yield y + +# Cross-scope reference (closure) +def foo(): + x = 1 + def inner(): + print(x) + yield x + +# Cross-scope reference (comprehension) +def foo(): + x = 1 + _ = [i for i in x] + yield x + +# Yield from with non-name value +def foo(): + yield from [1, 2, 3] + +# Yield non-name value +def foo(): + yield 1 + +# Variable used in nested function after yield +def foo(): + x = compute() + yield x + def bar(): + return x + +# Yield non-name value preceded by assignment +def foo(): + x = 1 + yield x + 1 + +# Yield from non-name value preceded by assignment +def foo(): + x = [1, 2, 3] + yield from [1, 2, 3] + +# Annotated assignment with value (not a plain assignment) +def foo(): + x: int = 1 + yield x + +# Assignment inside `with` using `contextlib.suppress` (body may not execute) +import contextlib + +def foo(): + x = default() + with contextlib.suppress(Exception): + x = something() + yield x diff --git a/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs b/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs index 8023ed32e4386..398e0f2692187 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs @@ -26,6 +26,7 @@ pub(crate) fn bindings(checker: &Checker) { Rule::CustomTypeVarForSelf, Rule::PrivateTypeParameter, Rule::UnnecessaryAssign, + Rule::UnnecessaryAssignBeforeYield, ]) { return; } @@ -39,6 +40,14 @@ pub(crate) fn bindings(checker: &Checker) { ); } } + if checker.is_rule_enabled(Rule::UnnecessaryAssignBeforeYield) { + if binding.kind.is_function_definition() { + ruff::rules::unnecessary_assign_before_yield( + checker, + binding.statement(checker.semantic()).unwrap(), + ); + } + } if checker.is_rule_enabled(Rule::UnusedVariable) { if binding.kind.is_bound_exception() && binding.is_unused() diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 28a7f01da1eb4..3d70edfc5d66d 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1064,6 +1064,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "067") => rules::ruff::rules::NonEmptyInitModule, (Ruff, "068") => rules::ruff::rules::DuplicateEntryInDunderAll, (Ruff, "069") => rules::ruff::rules::FloatEqualityComparison, + (Ruff, "070") => rules::ruff::rules::UnnecessaryAssignBeforeYield, (Ruff, "100") => rules::ruff::rules::UnusedNOQA, (Ruff, "101") => rules::ruff::rules::RedirectedNOQA, diff --git a/crates/ruff_linter/src/rules/flake8_return/mod.rs b/crates/ruff_linter/src/rules/flake8_return/mod.rs index 871e3af3c2690..f8370dd8ccbec 100644 --- a/crates/ruff_linter/src/rules/flake8_return/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_return/mod.rs @@ -4,6 +4,8 @@ mod helpers; pub(crate) mod rules; mod visitor; +pub(crate) use visitor::has_conditional_body; + #[cfg(test)] mod tests { use std::path::Path; diff --git a/crates/ruff_linter/src/rules/flake8_return/visitor.rs b/crates/ruff_linter/src/rules/flake8_return/visitor.rs index e939209f53665..d06894c6dfe0a 100644 --- a/crates/ruff_linter/src/rules/flake8_return/visitor.rs +++ b/crates/ruff_linter/src/rules/flake8_return/visitor.rs @@ -207,7 +207,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { /// data = data.decode() /// return data /// ``` -fn has_conditional_body(with: &ast::StmtWith, semantic: &SemanticModel) -> bool { +pub(crate) fn has_conditional_body(with: &ast::StmtWith, semantic: &SemanticModel) -> bool { with.items.iter().any(|item| { let ast::WithItem { context_expr: Expr::Call(ast::ExprCall { func, .. }), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index e56e0dbab75da..552b8b1095ca2 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -629,6 +629,7 @@ mod tests { #[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))] #[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))] #[test_case(Rule::FloatEqualityComparison, Path::new("RUF069.py"))] + #[test_case(Rule::UnnecessaryAssignBeforeYield, Path::new("RUF070.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index e0640d1b225e5..06cde1ce691ef 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -51,6 +51,7 @@ pub(crate) use static_key_dict_comprehension::*; #[cfg(any(feature = "test-rules", test))] pub(crate) use test_rules::*; pub(crate) use unmatched_suppression_comment::*; +pub(crate) use unnecessary_assign_before_yield::*; pub(crate) use unnecessary_cast_to_int::*; pub(crate) use unnecessary_iterable_allocation_for_first_element::*; pub(crate) use unnecessary_key_check::*; @@ -123,6 +124,7 @@ mod suppression_comment_visitor; #[cfg(any(feature = "test-rules", test))] pub(crate) mod test_rules; mod unmatched_suppression_comment; +mod unnecessary_assign_before_yield; mod unnecessary_cast_to_int; mod unnecessary_iterable_allocation_for_first_element; mod unnecessary_key_check; diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_assign_before_yield.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_assign_before_yield.rs new file mode 100644 index 0000000000000..b968d6fafcad4 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_assign_before_yield.rs @@ -0,0 +1,237 @@ +use anyhow::Context; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::token::TokenKind; +use ruff_python_ast::visitor; +use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::{self as ast, Expr, Identifier, Stmt}; +use ruff_python_semantic::SemanticModel; +use ruff_text_size::{Ranged, TextRange}; +use rustc_hash::FxHashSet; + +use crate::checkers::ast::Checker; +use crate::fix::edits; +use crate::rules::flake8_return::has_conditional_body; +use crate::{AlwaysFixableViolation, Edit, Fix}; + +/// ## What it does +/// Checks for variable assignments that immediately precede a `yield` (or +/// `yield from`) of the assigned variable, where the variable is not +/// referenced anywhere else. +/// +/// ## Why is this bad? +/// The variable assignment is not necessary, as the value can be yielded +/// directly. +/// +/// ## Example +/// ```python +/// def gen(): +/// x = 1 +/// yield x +/// ``` +/// +/// Use instead: +/// ```python +/// def gen(): +/// yield 1 +/// ``` +/// +/// ## Fix safety +/// This fix is always marked as unsafe because removing the intermediate +/// variable assignment changes the local variable bindings visible to +/// `locals()` and debuggers when the generator is suspended at the `yield`. +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] +pub(crate) struct UnnecessaryAssignBeforeYield { + name: String, + is_yield_from: bool, +} + +impl AlwaysFixableViolation for UnnecessaryAssignBeforeYield { + #[derive_message_formats] + fn message(&self) -> String { + let UnnecessaryAssignBeforeYield { + name, + is_yield_from, + } = self; + if *is_yield_from { + format!("Unnecessary assignment to `{name}` before `yield from` statement") + } else { + format!("Unnecessary assignment to `{name}` before `yield` statement") + } + } + + fn fix_title(&self) -> String { + "Remove unnecessary assignment".to_string() + } +} + +/// RUF070 +pub(crate) fn unnecessary_assign_before_yield(checker: &Checker, function_stmt: &Stmt) { + let Stmt::FunctionDef(function_def) = function_stmt else { + return; + }; + + let Some(function_scope) = checker.semantic().function_scope(function_def) else { + return; + }; + + let visitor = { + let mut visitor = YieldVisitor::new(checker.semantic()); + visitor.visit_body(&function_def.body); + visitor + }; + + for (assign, yield_expr, stmt) in &visitor.assignment_yield { + let (value, is_yield_from) = match yield_expr { + Expr::Yield(ast::ExprYield { + value: Some(value), .. + }) => (value.as_ref(), false), + Expr::YieldFrom(ast::ExprYieldFrom { value, .. }) => (value.as_ref(), true), + _ => continue, + }; + + let Expr::Name(ast::ExprName { id: yielded_id, .. }) = value else { + continue; + }; + + if let [Expr::Name(ast::ExprName { + id: assigned_id, .. + })] = assign.targets.as_slice() + && yielded_id == assigned_id + && !visitor.annotations.contains(assigned_id.as_str()) + && !visitor.non_locals.contains(assigned_id.as_str()) + && let Some(assigned_binding) = function_scope + .get(assigned_id) + .map(|binding_id| checker.semantic().binding(binding_id)) + // Unlike `return`, `yield` doesn't exit the function, so the variable could be + // referenced elsewhere. Only flag if the binding has exactly one reference (the + // yield itself). + && assigned_binding.references().count() == 1 + && assigned_binding + .references() + .map(|reference_id| checker.semantic().reference(reference_id)) + .all(|reference| reference.scope_id() == assigned_binding.scope) + { + checker + .report_diagnostic( + UnnecessaryAssignBeforeYield { + name: assigned_id.to_string(), + is_yield_from, + }, + value.range(), + ) + .try_set_fix(|| { + let delete_yield = + edits::delete_stmt(stmt, None, checker.locator(), checker.indexer()); + + let eq_token = checker + .tokens() + .before(assign.value.start()) + .iter() + .rfind(|token| token.kind() == TokenKind::Equal) + .context("Expected an equals token")?; + + let keyword = if is_yield_from { "yield from" } else { "yield" }; + let needs_parens = + matches!(assign.value.as_ref(), Expr::Yield(_) | Expr::YieldFrom(_)); + + let replace_assign = Edit::range_replacement( + if eq_token.end() < assign.value.start() { + keyword.to_string() + } else { + format!("{keyword} ") + }, + TextRange::new(assign.start(), eq_token.range().end()), + ); + + let mut edits = vec![replace_assign, delete_yield]; + if needs_parens { + edits.push(Edit::insertion("(".to_string(), assign.value.start())); + edits.push(Edit::insertion(")".to_string(), assign.value.end())); + } + + Ok(Fix::unsafe_edits(edits.remove(0), edits)) + }); + } + } +} + +struct YieldVisitor<'semantic, 'a> { + /// The semantic model of the current file. + semantic: &'semantic SemanticModel<'a>, + /// The non-local variables in the current function. + non_locals: FxHashSet<&'a str>, + /// The annotated variables in the current function. + annotations: FxHashSet<&'a str>, + /// The `assignment`-to-`yield` statement pairs in the current function. + assignment_yield: Vec<(&'a ast::StmtAssign, &'a Expr, &'a Stmt)>, + /// The preceding sibling of the current node. + sibling: Option<&'a Stmt>, +} + +impl<'semantic, 'a> YieldVisitor<'semantic, 'a> { + fn new(semantic: &'semantic SemanticModel<'a>) -> Self { + Self { + semantic, + non_locals: FxHashSet::default(), + annotations: FxHashSet::default(), + assignment_yield: Vec::new(), + sibling: None, + } + } +} + +impl<'a> Visitor<'a> for YieldVisitor<'_, 'a> { + fn visit_stmt(&mut self, stmt: &'a Stmt) { + match stmt { + Stmt::ClassDef(_) | Stmt::FunctionDef(_) => { + // Do not recurse into nested class/function bodies. + self.sibling = Some(stmt); + return; + } + Stmt::Global(ast::StmtGlobal { names, .. }) + | Stmt::Nonlocal(ast::StmtNonlocal { names, .. }) => { + self.non_locals.extend(names.iter().map(Identifier::as_str)); + } + Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => { + // Ex) `x: int` + if value.is_none() + && let Expr::Name(name) = target.as_ref() + { + self.annotations.insert(name.id.as_str()); + } + } + Stmt::Expr(ast::StmtExpr { value, .. }) => { + if matches!(value.as_ref(), Expr::Yield(_) | Expr::YieldFrom(_)) { + match self.sibling { + Some(Stmt::Assign(stmt_assign)) => { + self.assignment_yield + .push((stmt_assign, value.as_ref(), stmt)); + } + Some(Stmt::With(with)) => { + if let Some(stmt_assign) = + with.body.last().and_then(Stmt::as_assign_stmt) + && !has_conditional_body(with, self.semantic) + { + self.assignment_yield + .push((stmt_assign, value.as_ref(), stmt)); + } + } + _ => {} + } + } + } + _ => {} + } + + self.sibling = Some(stmt); + visitor::walk_stmt(self, stmt); + } + + fn visit_body(&mut self, body: &'a [Stmt]) { + let sibling = self.sibling; + self.sibling = None; + visitor::walk_body(self, body); + self.sibling = sibling; + } +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap new file mode 100644 index 0000000000000..0ff182c8e69bd --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap @@ -0,0 +1,286 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:7:11 + | +5 | def foo(): +6 | x = 1 +7 | yield x # RUF070 + | ^ +8 | +9 | def foo(): + | +help: Remove unnecessary assignment +3 | ### +4 | +5 | def foo(): + - x = 1 + - yield x # RUF070 +6 + yield 1 +7 | +8 | def foo(): +9 | x = [1, 2, 3] +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield from` statement + --> RUF070.py:11:16 + | + 9 | def foo(): +10 | x = [1, 2, 3] +11 | yield from x # RUF070 + | ^ +12 | +13 | def foo(): + | +help: Remove unnecessary assignment +7 | yield x # RUF070 +8 | +9 | def foo(): + - x = [1, 2, 3] + - yield from x # RUF070 +10 + yield from [1, 2, 3] +11 | +12 | def foo(): +13 | for i in range(10): +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:16:15 + | +14 | for i in range(10): +15 | x = i * 2 +16 | yield x # RUF070 + | ^ +17 | +18 | def foo(): + | +help: Remove unnecessary assignment +12 | +13 | def foo(): +14 | for i in range(10): + - x = i * 2 + - yield x # RUF070 +15 + yield i * 2 +16 | +17 | def foo(): +18 | if True: +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:21:15 + | +19 | if True: +20 | x = 1 +21 | yield x # RUF070 + | ^ +22 | +23 | def foo(): + | +help: Remove unnecessary assignment +17 | +18 | def foo(): +19 | if True: + - x = 1 + - yield x # RUF070 +20 + yield 1 +21 | +22 | def foo(): +23 | with open("foo.txt") as f: +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:26:15 + | +24 | with open("foo.txt") as f: +25 | x = f.read() +26 | yield x # RUF070 + | ^ +27 | +28 | def foo(): + | +help: Remove unnecessary assignment +22 | +23 | def foo(): +24 | with open("foo.txt") as f: + - x = f.read() + - yield x # RUF070 +25 + yield f.read() +26 | +27 | def foo(): +28 | try: +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:31:15 + | +29 | try: +30 | x = something() +31 | yield x # RUF070 + | ^ +32 | except Exception: +33 | pass + | +help: Remove unnecessary assignment +27 | +28 | def foo(): +29 | try: + - x = something() + - yield x # RUF070 +30 + yield something() +31 | except Exception: +32 | pass +33 | +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:37:11 + | +35 | def foo(): +36 | x = some_function() +37 | yield x # RUF070 + | ^ +38 | +39 | def foo(): + | +help: Remove unnecessary assignment +33 | pass +34 | +35 | def foo(): + - x = some_function() + - yield x # RUF070 +36 + yield some_function() +37 | +38 | def foo(): +39 | x = some_generator() +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield from` statement + --> RUF070.py:41:16 + | +39 | def foo(): +40 | x = some_generator() +41 | yield from x # RUF070 + | ^ +42 | +43 | def foo(): + | +help: Remove unnecessary assignment +37 | yield x # RUF070 +38 | +39 | def foo(): + - x = some_generator() + - yield from x # RUF070 +40 + yield from some_generator() +41 | +42 | def foo(): +43 | x = lambda: 1 +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:45:11 + | +43 | def foo(): +44 | x = lambda: 1 +45 | yield x # RUF070 + | ^ +46 | +47 | def foo(): + | +help: Remove unnecessary assignment +41 | yield from x # RUF070 +42 | +43 | def foo(): + - x = lambda: 1 + - yield x # RUF070 +44 + yield lambda: 1 +45 | +46 | def foo(): +47 | x = (y := 1) +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:49:11 + | +47 | def foo(): +48 | x = (y := 1) +49 | yield x # RUF070 + | ^ +50 | +51 | def foo(): + | +help: Remove unnecessary assignment +45 | yield x # RUF070 +46 | +47 | def foo(): + - x = (y := 1) + - yield x # RUF070 +48 + yield (y := 1) +49 | +50 | def foo(): +51 | x =1 +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:53:11 + | +51 | def foo(): +52 | x =1 +53 | yield x # RUF070 (no space after `=`) + | ^ +54 | +55 | def foo(): + | +help: Remove unnecessary assignment +49 | yield x # RUF070 +50 | +51 | def foo(): + - x =1 + - yield x # RUF070 (no space after `=`) +52 + yield 1 +53 | +54 | def foo(): +55 | x = yield 1 +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:57:11 + | +55 | def foo(): +56 | x = yield 1 +57 | yield x # RUF070 (yield as assigned value, fix adds parentheses) + | ^ +58 | +59 | # Assignment inside `with`, yield outside + | +help: Remove unnecessary assignment +53 | yield x # RUF070 (no space after `=`) +54 | +55 | def foo(): + - x = yield 1 + - yield x # RUF070 (yield as assigned value, fix adds parentheses) +56 + yield (yield 1) +57 | +58 | # Assignment inside `with`, yield outside +59 | def foo(): +note: This is an unsafe fix and may change runtime behavior + +RUF070 [*] Unnecessary assignment to `x` before `yield` statement + --> RUF070.py:63:11 + | +61 | with open("foo.txt") as f: +62 | x = f.read() +63 | yield x # RUF070 + | ^ + | +help: Remove unnecessary assignment +59 | # Assignment inside `with`, yield outside +60 | def foo(): +61 | with open("foo.txt") as f: + - x = f.read() + - yield x # RUF070 +62 + yield f.read() +63 | +64 | +65 | ### +note: This is an unsafe fix and may change runtime behavior diff --git a/ruff.schema.json b/ruff.schema.json index 8a211304a4b1d..0f1e7cf4d60f0 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4139,6 +4139,8 @@ "RUF067", "RUF068", "RUF069", + "RUF07", + "RUF070", "RUF1", "RUF10", "RUF100", From 1d94f26cad8678e712cc274891bf19f7a700d212 Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Fri, 20 Feb 2026 16:12:21 +0100 Subject: [PATCH 019/261] [`pyupgrade`] Fix handling of `\N` in raw strings (`UP032`) (#22149) **Summary:** Fixes UP032 autofix incorrectly converting raw strings with `\N{...}` to f-strings, which changes semantics and causes runtime errors. Fixes #22060 ## Test Plan - Added test case for raw strings with \N{...} - Regular strings with \N{...} still autofix correctly - All 119 pyupgrade tests pass --------- Co-authored-by: Brent Westbrook --- .../test/fixtures/pyupgrade/UP032_0.py | 3 ++ .../src/rules/pyupgrade/rules/f_strings.rs | 6 ++- ...__rules__pyupgrade__tests__UP032_0.py.snap | 19 ++++++++++ crates/ruff_python_literal/src/format.rs | 37 ++++++++++++------- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py index 3214801237aae..24870c52c48d3 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py @@ -278,3 +278,6 @@ async def c(): # Unicode escape "\N{angle}AOB = {angle}°".format(angle=180) + +# Raw string with \N{...} +r"\N{angle}AOB = {angle}°".format(angle=180) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs index 9749969691d13..88374e5d84e2f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs @@ -275,7 +275,11 @@ impl FStringConversion { } // Parse the format string. - let format_string = FormatString::from_str(contents)?; + let format_string = if raw { + FormatString::from_raw_str(contents) + } else { + FormatString::from_str(contents) + }?; // If the format string contains only literal parts, it doesn't need to be converted. if format_string diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap index f15311cad0975..119dd00c447e0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap @@ -1378,6 +1378,8 @@ UP032 [*] Use f-string instead of `format` call 279 | # Unicode escape 280 | "\N{angle}AOB = {angle}°".format(angle=180) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +281 | +282 | # Raw string with \N{...} | help: Convert to f-string 277 | print(string) @@ -1385,3 +1387,20 @@ help: Convert to f-string 279 | # Unicode escape - "\N{angle}AOB = {angle}°".format(angle=180) 280 + f"\N{angle}AOB = {180}°" +281 | +282 | # Raw string with \N{...} +283 | r"\N{angle}AOB = {angle}°".format(angle=180) + +UP032 [*] Use f-string instead of `format` call + --> UP032_0.py:283:1 + | +282 | # Raw string with \N{...} +283 | r"\N{angle}AOB = {angle}°".format(angle=180) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Convert to f-string +280 | "\N{angle}AOB = {angle}°".format(angle=180) +281 | +282 | # Raw string with \N{...} + - r"\N{angle}AOB = {angle}°".format(angle=180) +283 + rf"\N{180}AOB = {180}°" diff --git a/crates/ruff_python_literal/src/format.rs b/crates/ruff_python_literal/src/format.rs index 687694c8bc9d1..f1eba8e8f54ed 100644 --- a/crates/ruff_python_literal/src/format.rs +++ b/crates/ruff_python_literal/src/format.rs @@ -589,12 +589,14 @@ impl FormatString { Ok((first_char, chars.as_str())) } - fn parse_literal(text: &str) -> Result<(FormatPart, &str), FormatParseError> { + fn parse_literal(text: &str, is_raw: bool) -> Result<(FormatPart, &str), FormatParseError> { let mut cur_text = text; let mut result_string = String::new(); let mut pending_escape = false; while !cur_text.is_empty() { - if pending_escape + // Raw strings: \N{...} is literal, not a Unicode escape + if !is_raw + && pending_escape && let Some((unicode_string, remaining)) = FormatString::parse_escaped_unicode_string(cur_text) { @@ -697,23 +699,14 @@ impl FormatString { (&text[..end_idx], &text[end_idx..]) }) } -} - -pub trait FromTemplate<'a>: Sized { - type Err; - fn from_str(s: &'a str) -> Result; -} -impl<'a> FromTemplate<'a> for FormatString { - type Err = FormatParseError; - - fn from_str(text: &'a str) -> Result { + fn parse(text: &str, is_raw: bool) -> Result { let mut cur_text: &str = text; let mut parts: Vec = Vec::new(); while !cur_text.is_empty() { // Try to parse both literals and bracketed format parts until we // run out of text - cur_text = FormatString::parse_literal(cur_text) + cur_text = FormatString::parse_literal(cur_text, is_raw) .or_else(|_| FormatString::parse_spec(cur_text, AllowPlaceholderNesting::Yes)) .map(|(part, new_text)| { parts.push(part); @@ -726,6 +719,24 @@ impl<'a> FromTemplate<'a> for FormatString { } } +pub trait FromTemplate<'a>: Sized { + type Err; + fn from_str(s: &'a str) -> Result; + fn from_raw_str(s: &'a str) -> Result; +} + +impl<'a> FromTemplate<'a> for FormatString { + type Err = FormatParseError; + + fn from_str(text: &'a str) -> Result { + FormatString::parse(text, false) + } + + fn from_raw_str(text: &'a str) -> Result { + FormatString::parse(text, true) + } +} + #[cfg(test)] mod tests { use super::*; From b9e29ddd1e67878c01399b25b664a579748760fe Mon Sep 17 00:00:00 2001 From: Assad Yousuf <45297189+assadyousuf@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:30:11 -0700 Subject: [PATCH 020/261] [`flake8-bugbear`] Allow `B901` in pytest hook wrappers (#21931) ## Summary Stop raising return-in-generator with pytest hook wrappers (@hookimpl(wrapper=True)). They are specifically designed to use this pattern: https://docs.pytest.org/en/stable/how-to/writing_hook_functions.html#hook-wrappers-executing-around-other-hooks Before: return-in-generator reports would surface with pytest hook wrappers After: specifically check for pytest hook wrappers before reporting return-in-generator Testing: Wrote some tests to cover different cases --------- Co-authored-by: Brent Westbrook --- .../test/fixtures/flake8_bugbear/B901.py | 40 +++++++++++++++++++ .../rules/return_in_generator.rs | 9 +++++ ...__flake8_bugbear__tests__B901_B901.py.snap | 27 +++++++++++++ .../src/rules/flake8_pytest_style/helpers.rs | 29 +++++++++++++- .../src/rules/flake8_pytest_style/mod.rs | 2 +- 5 files changed, 105 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B901.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B901.py index acb932f25fd89..c747d265b0cb1 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B901.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B901.py @@ -86,3 +86,43 @@ async def broken6(): async def broken7(): yield 1 return [1, 2, 3] + + +import pytest + + +@pytest.hookimpl(wrapper=True) +def pytest_runtest_makereport(): + result = yield + return result + + +@pytest.hookimpl(wrapper=True) +def pytest_fixture_setup(): + result = yield + result.some_attr = "modified" + return result + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_call(): + result = yield + return result + + +@pytest.hookimpl() +def pytest_configure(): + yield + return "should error" + + +@pytest.hookimpl(wrapper=False) +def pytest_unconfigure(): + yield + return "should error" + + +@pytest.fixture() +def my_fixture(): + yield + return "should error" diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/return_in_generator.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/return_in_generator.rs index 0b089b34595c8..d3aa9fea8dfd4 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/return_in_generator.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/return_in_generator.rs @@ -5,6 +5,7 @@ use ruff_text_size::TextRange; use crate::Violation; use crate::checkers::ast::Checker; +use crate::rules::flake8_pytest_style::helpers::is_pytest_hookimpl_wrapper; /// ## What it does /// Checks for `return {value}` statements in functions that also contain `yield` @@ -100,6 +101,14 @@ pub(crate) fn return_in_generator(checker: &Checker, function_def: &StmtFunction return; } + if function_def + .decorator_list + .iter() + .any(|decorator| is_pytest_hookimpl_wrapper(decorator, checker.semantic())) + { + return; + } + let mut visitor = ReturnInGeneratorVisitor::default(); visitor.visit_body(&function_def.body); diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B901_B901.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B901_B901.py.snap index 21bf1b1645a97..666db24c08f75 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B901_B901.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B901_B901.py.snap @@ -64,3 +64,30 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 88 | return [1, 2, 3] | ^^^^^^^^^^^^^^^^ | + +B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior + --> B901.py:116:5 + | +114 | def pytest_configure(): +115 | yield +116 | return "should error" + | ^^^^^^^^^^^^^^^^^^^^^ + | + +B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior + --> B901.py:122:5 + | +120 | def pytest_unconfigure(): +121 | yield +122 | return "should error" + | ^^^^^^^^^^^^^^^^^^^^^ + | + +B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior + --> B901.py:128:5 + | +126 | def my_fixture(): +127 | yield +128 | return "should error" + | ^^^^^^^^^^^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/helpers.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/helpers.rs index 43429c8c4ff93..b0b72868e1c33 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/helpers.rs @@ -1,6 +1,6 @@ use std::fmt; -use ruff_python_ast::helpers::map_callable; +use ruff_python_ast::helpers::{is_const_true, map_callable}; use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall, Keyword, Stmt, StmtFunctionDef}; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::{ScopeKind, SemanticModel}; @@ -50,6 +50,33 @@ pub(super) fn is_pytest_parametrize(call: &ExprCall, semantic: &SemanticModel) - }) } +/// Returns `true` if the decorator is `@pytest.hookimpl(wrapper=True)` or +/// `@pytest.hookimpl(hookwrapper=True)`. +/// +/// These hook wrappers intentionally use `return` in generator functions as part of the +/// pytest hook wrapper protocol. +/// +/// See: +pub(crate) fn is_pytest_hookimpl_wrapper(decorator: &Decorator, semantic: &SemanticModel) -> bool { + let Expr::Call(call) = &decorator.expression else { + return false; + }; + + // Check if it's pytest.hookimpl + let is_hookimpl = semantic + .resolve_qualified_name(&call.func) + .is_some_and(|name| matches!(name.segments(), ["pytest", "hookimpl"])); + + if !is_hookimpl { + return false; + } + + let wrapper = call.arguments.find_argument_value("wrapper", 6); + let hookwrapper = call.arguments.find_argument_value("hookwrapper", 1); + + wrapper.or(hookwrapper).is_some_and(is_const_true) +} + /// Whether the currently checked `func` is likely to be a Pytest test. /// /// A normal Pytest test function is one whose name starts with `test` and is either: diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs index 5923d978ec7a1..0c0ba0a7e11c5 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs @@ -1,5 +1,5 @@ //! Rules from [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/). -mod helpers; +pub(crate) mod helpers; pub(crate) mod rules; pub mod settings; pub mod types; From f80f6b77adcb0eaccbf5f38314b26e1f8cc46bdf Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 20 Feb 2026 10:10:26 -0500 Subject: [PATCH 021/261] [ty] Fix bug where diagnostics could disappear after opening an external file This happened whenever: * Diagnostic mode was set to `openFilesOnly` * One opened and then closed an external file (or caused the LSP client to send notifications that a file was closed, which may happen even when the end user doesn't actually open or close a file) When this happened, the entire open file set was cleared. This in turn resulted in `should_check_file` returning `false` for any file previously in the open file set. And thus, we'd never get any diagnostics. Looking at our revision history, it seems like this bug has been present for a long time? I'm not sure. However, this bug doesn't apply when the diagnostic mode is set to `workspace`, which might explain why it defied reproduction. I've also included a regression test and some extra TRACE level logs that might help diagnose these sorts of problems in the future. Fixes https://github.com/astral-sh/ty-vscode/issues/342 --- crates/ty_project/src/lib.rs | 39 +++++++++--- .../ty_server/tests/e2e/pull_diagnostics.rs | 63 +++++++++++++++++++ .../ty_server/tests/e2e/workspace_folders.rs | 4 +- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index 6f4e3e1329203..dd6b7a7ed0071 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -365,10 +365,7 @@ impl Project { let mut open_files = self.take_open_files(db); let removed = open_files.remove(&file); - - if removed { - self.set_open_files(db, open_files); - } + self.set_open_files(db, open_files); removed } @@ -453,14 +450,34 @@ impl Project { pub fn should_check_file(self, db: &dyn Db, file: File) -> bool { let path = file.path(db); + // NOTE: The tracing messages below were added because + // whether a file should be checked or not can sometimes + // be at the root of confusing UX like "diagnostics all + // of a sudden stopped working." Having a trace message + // indicating *why* a particular file isn't being checked + // can be quite helpful for narrowing down the issue. + // + // The problem is that it's incredibly noisy. Which is why + // we set them to the TRACE level. + // Try to return early to avoid adding a dependency on `open_files` or `file_set` which // both have a durability of `LOW`. if path.is_vendored_path() { + tracing::trace!("Not checking {path} because it is a vendored path"); return false; } match self.check_mode(db) { - CheckMode::OpenFiles => self.open_files(db).contains(&file), + CheckMode::OpenFiles => { + let should_check = self.open_files(db).contains(&file); + if !should_check { + tracing::trace!( + "Not checking {path} because check mode is `OpenFiles` \ + and it is not in the open file set" + ); + } + should_check + } CheckMode::AllFiles => { // Virtual files are always checked. // @@ -476,9 +493,17 @@ impl Project { // neovim uses `file://...` even for an open buffer // that does not correspond to a file saved to disk // yet. - path.is_system_virtual_path() + let should_check = path.is_system_virtual_path() || self.files(db).contains(&file) - || self.open_files(db).contains(&file) + || self.open_files(db).contains(&file); + if !should_check { + tracing::trace!( + "Not checking {path} because check mode is `AllFiles` \ + and it is not a virtual path, in the project files \ + or in the open file set" + ); + } + should_check } } } diff --git a/crates/ty_server/tests/e2e/pull_diagnostics.rs b/crates/ty_server/tests/e2e/pull_diagnostics.rs index 96c43b27746c2..dc749a046d684 100644 --- a/crates/ty_server/tests/e2e/pull_diagnostics.rs +++ b/crates/ty_server/tests/e2e/pull_diagnostics.rs @@ -11,6 +11,7 @@ use lsp_types::{ use ruff_db::system::SystemPath; use ty_server::{ClientOptions, DiagnosticMode, PartialWorkspaceProgress}; +use crate::workspace_folders::condensed_document_diagnostic_snapshot; use crate::{AwaitResponseError, TestServer, TestServerBuilder}; #[test] @@ -1032,6 +1033,68 @@ def hello() -> str: Ok(()) } +/// Regression test for diagnostics disappearing in some cases. +/// +/// The specific way this fails is when a file that was never in the "open +/// file set" is closed. When that happens, there was a bug where the +/// open file set was completely cleared. This in turn would result in +/// `Project::should_check_file` returning `false` for any other open file. And +/// that would finally result in an empty set of diagnostics being returned, +/// which would effectively clear any existing diagnostics. +/// +/// Moreover, since the file was no longer in the open set, there's likely +/// other mysterious failures happening. +/// +/// See: +#[test] +fn closing_external_file_preserves_open_files() -> Result<()> { + let _filter = filter_result_id(); + + let workspace_root = SystemPath::new("src"); + let main_path = SystemPath::new("src/main.py"); + let main_content = "\ +def foo() -> str: + return 42 # intentional type error to provoke some diagnostics +"; + + let mut server = TestServerBuilder::new()? + .with_workspace(workspace_root, None)? + .with_file(main_path, main_content)? + .enable_pull_diagnostics(true) + .build() + .wait_until_workspaces_are_initialized(); + + // Assert that we get diagnostics as expected. + server.open_text_document(main_path, main_content, 1); + let diagnostics_before = server.document_diagnostic_request(main_path, None); + insta::assert_snapshot!( + condensed_document_diagnostic_snapshot(diagnostics_before), + @"1:11..1:13[ERROR]: Return type does not match returned value: expected `str`, found `Literal[42]`", + ); + + // Open an "external" file, e.g., this is what happens + // when a user does goto definition on `str`. + // + // The path has to be outside of the workspace root so + // that the file is not considered part of the open file + // set. + server.open_text_document("external/builtins.pyi", "class str: ...", 1); + // This is what had resulted in the open file set being + // completely cleared. + server.close_text_document("external/builtins.pyi"); + + // Now request diagnostics again. We should get back the same + // diagnostics as above. The bug here resulted in no diagnostics + // being returned. + let diagnostics_after = server.document_diagnostic_request(main_path, None); + insta::assert_snapshot!( + condensed_document_diagnostic_snapshot(diagnostics_after), + @"1:11..1:13[ERROR]: Return type does not match returned value: expected `str`, found `Literal[42]`", + ); + + Ok(()) +} + // Helper functions for long-polling tests fn create_workspace_server_with_file( workspace_root: &SystemPath, diff --git a/crates/ty_server/tests/e2e/workspace_folders.rs b/crates/ty_server/tests/e2e/workspace_folders.rs index 3320e3e2cc9ba..87962018b17ee 100644 --- a/crates/ty_server/tests/e2e/workspace_folders.rs +++ b/crates/ty_server/tests/e2e/workspace_folders.rs @@ -649,7 +649,9 @@ fn condensed_workspace_diagnostic_snapshot(report: WorkspaceDiagnosticReportResu .join("\n") } -fn condensed_document_diagnostic_snapshot(report: DocumentDiagnosticReportResult) -> String { +pub(crate) fn condensed_document_diagnostic_snapshot( + report: DocumentDiagnosticReportResult, +) -> String { match report { DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(full)) => { condensed_full_document_diagnostic_report(full.full_document_diagnostic_report) From c91d29fefbab45576828ea979fd8b0f1c0a3be8a Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 20 Feb 2026 17:25:18 +0100 Subject: [PATCH 022/261] [ty] Reachability analysis for generic function calls (#23419) ## Summary If a generic function's return type depends on a type variable, and the argument passed resolves that type variable to `Never`, the call should still be treated as terminal: ```py def identity[T](x: T) -> T: return x def f() -> Never: identity(exit()) # should be detected as terminal ``` This is a tiny win for correctness, but unfortunately a small drop in performance and slight increase in memory usage, because we need to infer more call expressions upfront. If we think it's not important enough, I'm also okay to change this to a test-only PR that documents this as a known limitation. If we merge it, I might follow up with an idea to simplify the code in a [slightly larger refactoring PR](https://github.com/astral-sh/ruff/pull/23378). ## Memory usage Insignificant changes on some large internal projects, a 2% *decrease* in memory usage when running on home-assistant/core. ## Ecosystem No ecosystem changes, as far as I can tell. ## Test Plan New Markdown tests --- .../resources/mdtest/attributes.md | 2 +- .../resources/mdtest/terminal_statements.md | 29 ++++++++++++++++++- .../reachability_constraints.rs | 24 +++++++-------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 19f1c218836c3..612bcac3495b6 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2699,7 +2699,7 @@ class ManyCycles2: def f1(self: "ManyCycles2"): # TODO: should be Unknown | list[Unknown | int] | list[Divergent] - reveal_type(self.x3) # revealed: Unknown | list[Unknown | int] | list[Divergent] | list[Divergent] + reveal_type(self.x3) # revealed: Unknown | list[Unknown | int] | list[Unknown] | list[Divergent] self.x1 = [self.x2] + [self.x3] self.x2 = [self.x1] + [self.x3] diff --git a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md index 63abf0e847182..762a77746ee1f 100644 --- a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md +++ b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md @@ -755,9 +755,36 @@ def _() -> NoReturn: f("") ``` +### Generic functions + +If a generic function's return type depends on a type variable, and the argument passed resolves +that type variable to `Never`, the call should still be treated as terminal. + +```py +from typing import TypeVar, NoReturn + +T = TypeVar("T") + +def identity(x: T) -> T: + return x + +# No "implicitly returns `None`" diagnostic +def _() -> NoReturn: + identity(exit()) + +def _(flag: bool): + if flag: + x = "test" + else: + x = "terminal" + identity(exit()) + + reveal_type(x) # revealed: Literal["test"] +``` + ### Other callables -If other types of callables are annotated with `NoReturn`, we should still be ablt to infer correct +If other types of callables are annotated with `NoReturn`, we should still be able to infer correct reachability. ```py diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 2ddae26406dd4..647bb088d1582 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -1100,18 +1100,18 @@ impl ReachabilityConstraints { return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive); }; - let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator - .fold((true, true), |(none, all), overload| { - let overload_returns_never = - overload.return_ty.is_equivalent_to(db, Type::Never); - - ( - none && !overload_returns_never, - all && overload_returns_never, - ) - }); - - if no_overloads_return_never { + let mut no_overloads_return_never = true; + let mut all_overloads_return_never = true; + let mut any_overload_is_generic = false; + + for overload in overloads_iterator { + let returns_never = overload.return_ty.is_equivalent_to(db, Type::Never); + no_overloads_return_never &= !returns_never; + all_overloads_return_never &= returns_never; + any_overload_is_generic |= overload.return_ty.has_typevar(db); + } + + if no_overloads_return_never && !any_overload_is_generic { Truthiness::AlwaysFalse } else if all_overloads_return_never { Truthiness::AlwaysTrue From 9719e17c256c7c0e3cc2188e51e52d814364fe03 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 20 Feb 2026 19:09:01 +0100 Subject: [PATCH 023/261] [ty] Diagnostic when combining `Final` and `ClassVar` (#23365) ## Summary This PR implements the following paragraph in the typing spec: > Type checkers should infer a final attribute that is initialized in a class body as being a class variable, except in the case of [Dataclasses](https://typing.python.org/en/latest/spec/dataclasses.html), where `x: Final[int] = 3` creates a dataclass field and instance-level final attribute `x` with default value `3`; `x: ClassVar[Final[int]] = 3` is necessary to create a final class variable with value `3`. In non-dataclasses, combining `ClassVar` and `Final` is redundant, and type checkers may choose to warn or error on the redundancy. > > https://typing.python.org/en/latest/spec/qualifiers.html#semantics-and-examples ## Test Plan New Markdown tests --- crates/ty/docs/rules.md | 226 ++++++++++-------- .../resources/mdtest/attributes.md | 4 + .../mdtest/type_qualifiers/classvar.md | 111 +++++++++ .../resources/mdtest/type_qualifiers/final.md | 11 +- crates/ty_python_semantic/src/types/class.rs | 11 + .../src/types/diagnostic.rs | 27 +++ .../infer/builder/annotation_expression.rs | 28 ++- scripts/conformance.py | 6 +- ty.schema.json | 10 + 9 files changed, 330 insertions(+), 104 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 11145116e7ce3..814eeae5068d0 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -640,7 +640,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -766,7 +766,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -796,7 +796,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -822,7 +822,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -856,7 +856,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -945,7 +945,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -972,7 +972,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1000,7 +1000,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1034,7 +1034,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1070,7 +1070,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1094,7 +1094,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1121,7 +1121,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1158,7 +1158,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1190,7 +1190,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1219,7 +1219,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1263,7 +1263,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1305,7 +1305,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1349,7 +1349,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1387,7 +1387,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1466,7 +1466,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1505,7 +1505,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1566,7 +1566,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1601,7 +1601,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1629,7 +1629,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1663,7 +1663,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1770,7 +1770,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1824,7 +1824,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1854,7 +1854,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1904,7 +1904,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1930,7 +1930,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1961,7 +1961,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1995,7 +1995,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2044,7 +2044,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2073,7 +2073,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2169,7 +2169,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2215,7 +2215,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2242,7 +2242,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2289,7 +2289,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2319,7 +2319,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2349,7 +2349,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2383,7 +2383,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2417,7 +2417,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2448,7 +2448,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2495,7 +2495,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2527,7 +2527,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2562,7 +2562,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2593,7 +2593,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2648,7 +2648,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2691,7 +2691,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2716,7 +2716,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2749,7 +2749,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2778,7 +2778,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2804,7 +2804,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2828,7 +2828,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2861,7 +2861,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2894,7 +2894,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2921,7 +2921,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2948,7 +2948,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2976,7 +2976,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3008,7 +3008,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3045,7 +3045,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3109,7 +3109,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3130,13 +3130,45 @@ def f() -> int: cast(int, f()) # Redundant ``` +## `redundant-final-classvar` + + +Default level: warn · +Added in 0.0.18 · +Related issues · +View source + + + +**What it does** + +Checks for redundant combinations of the `ClassVar` and `Final` type qualifiers. + +**Why is this bad?** + +An attribute that is marked `Final` in a class body is implicitly a class variable. +Marking it as `ClassVar` is therefore redundant. + +Note that this diagnostic is not emitted for dataclass fields, where +`ClassVar[Final[int]]` has a distinct meaning from `Final[int]`. + +**Examples** + +```python +from typing import ClassVar, Final + +class C: + x: ClassVar[Final[int]] = 1 # redundant + y: Final[ClassVar[int]] = 1 # redundant +``` + ## `static-assert-error` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3166,7 +3198,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3195,7 +3227,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3229,7 +3261,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3256,7 +3288,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3284,7 +3316,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3330,7 +3362,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3354,7 +3386,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3381,7 +3413,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3409,7 +3441,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3467,7 +3499,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3492,7 +3524,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3517,7 +3549,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3556,7 +3588,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3593,7 +3625,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3634,7 +3666,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3735,7 +3767,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3798,7 +3830,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 612bcac3495b6..e11d0e39f79b7 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -747,9 +747,13 @@ If a class variable is additionally qualified as `Final`, we do not union with ` from typing import Final class D: + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" final1: Final[ClassVar] = 1 + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" final2: ClassVar[Final] = 1 + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" final3: ClassVar[Final[int]] = 1 + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" final4: Final[ClassVar[int]] = 1 reveal_type(D.final1) # revealed: Literal[1] diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md index 2fb8e59e1b658..a226c3997ae20 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -201,6 +201,117 @@ class Sub(Base): ... reveal_type(Sub.all_instances) # revealed: list[Sub] ``` +## Combining `ClassVar` and `Final` in normal classes + +An attribute on a class body that is annotated as `Final` is implicitly treated as a class variable. +The error message is different, but these attributes cannot be written to from instances of the +class: + +```py +from typing import Final + +class C: + a: Final[int] = 1 + +reveal_type(C.a) # revealed: int + +c = C() +c.a = 2 # error: [invalid-assignment] "Cannot assign to final attribute `a` on type `C`" +``` + +In this sense, it is redundant to combine `ClassVar` and `Final`. We issue a warning in these cases: + +```py +from typing import Annotated, ClassVar + +class D: + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" + a: ClassVar[Final[int]] = 1 + + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" + b: Final[ClassVar[int]] = 1 + + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" + c: Final[ClassVar] = 1 + + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" + d: Annotated[Final[ClassVar[int]], "metadata"] = 1 + + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" + e: ClassVar[Final] = 1 + + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" + f: Annotated[Final[Annotated[Annotated[ClassVar[int], "a"], "b"]], "c"] = 1 + +reveal_type(D.a) # revealed: int +reveal_type(D.b) # revealed: int +reveal_type(D.c) # revealed: Literal[1] +reveal_type(D.d) # revealed: int +reveal_type(D.e) # revealed: Literal[1] +reveal_type(D.f) # revealed: int + +d = D() +d.a = 2 # error: [invalid-attribute-access] "Cannot assign to ClassVar `a` from an instance of type `D`" +d.b = 2 # error: [invalid-attribute-access] "Cannot assign to ClassVar `b` from an instance of type `D`" +d.c = 2 # error: [invalid-attribute-access] "Cannot assign to ClassVar `c` from an instance of type `D`" +d.d = 2 # error: [invalid-attribute-access] "Cannot assign to ClassVar `d` from an instance of type `D`" +d.e = 2 # error: [invalid-attribute-access] "Cannot assign to ClassVar `e` from an instance of type `D`" +d.f = 2 # error: [invalid-attribute-access] "Cannot assign to ClassVar `f` from an instance of type `D`" +``` + +## Combining `ClassVar` and `Final` in dataclasses + +In dataclasses, `ClassVar[Final[int]]` has a distinct meaning from `Final[int]`. The former is a +final class variable, the latter is a final instance attribute. The warning is therefore not emitted +when combining `ClassVar[Final[...]]` in dataclasses: + +```py +from dataclasses import dataclass +from typing import ClassVar, Final + +@dataclass +class D: + # No warning: + class_attr: ClassVar[Final[int]] = 1 + + instance_attr: Final[int] = 1 +``` + +Note that `class_attr` does not appear in the signature of `__init__`: + +```py +# revealed: (self: D, instance_attr: int = 1) -> None +reveal_type(D.__init__) +``` + +```py +def _(d: D): + reveal_type(d.class_attr) # revealed: int + reveal_type(d.instance_attr) # revealed: int + + d.class_attr = 2 # error: [invalid-attribute-access] +``` + +The reverse direction `Final[ClassVar[...]]` is not recognized by the runtime implementation of +dataclasses. We could consider emitting a warning in these cases, but for now, we treat is just like +`ClassVar[Final[...]]` and allow it in dataclasses: + +```py +from dataclasses import dataclass + +@dataclass +class E: + class_attr: Final[ClassVar[int]] = 1 + +# revealed: (self: E) -> None +reveal_type(E.__init__) + +def _(e: E): + reveal_type(e.class_attr) # revealed: int + + e.class_attr = 2 # error: [invalid-attribute-access] +``` + ## Illegal `ClassVar` in type expression ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index 967c28f5220ff..4bbac4edc0a98 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -460,6 +460,7 @@ class C(B): from typing import Final, ClassVar, Annotated class Base: + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" X: ClassVar[Final[int]] = 1 Y: Annotated[Final[int], "metadata"] = 2 @@ -581,14 +582,14 @@ LEGAL_D: Final LEGAL_D = 1 class C: + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" LEGAL_E: ClassVar[Final[int]] = 1 - LEGAL_F: Final[ClassVar[int]] = 1 - LEGAL_G: Annotated[Final[ClassVar[int]], "metadata"] = 1 + LEGAL_F: Annotated[Final[int], "metadata"] = 1 def __init__(self): - self.LEGAL_H: Final[int] = 1 - self.LEGAL_I: Final[int] - self.LEGAL_I = 1 + self.LEGAL_G: Final[int] = 1 + self.LEGAL_H: Final[int] + self.LEGAL_H = 1 # error: [invalid-type-form] "`Final` is not allowed in function parameter annotations" def f(ILLEGAL: Final[int]) -> None: diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 41814693e1dc9..80dffc7745511 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2203,6 +2203,17 @@ impl<'db> StaticClassLiteral<'db> { }) } + /// Returns `true` if this class is a dataclass-like class. + /// + /// This covers `@dataclass`-decorated classes, as well as classes created via + /// `dataclass_transform` (function-based, metaclass-based, and base-class-based). + pub(crate) fn is_dataclass_like(self, db: &'db dyn Db) -> bool { + matches!( + CodeGeneratorKind::from_class(db, ClassLiteral::Static(self), None), + Some(CodeGeneratorKind::DataclassLike(_)) + ) + } + /// Returns a new [`StaticClassLiteral`] with the given dataclass params, preserving all other fields. pub(crate) fn with_dataclass_params( self, diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index c268363d2e5e9..6a828626ef47e 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -141,6 +141,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&STATIC_ASSERT_ERROR); registry.register_lint(&INVALID_ATTRIBUTE_ACCESS); registry.register_lint(&REDUNDANT_CAST); + registry.register_lint(&REDUNDANT_FINAL_CLASSVAR); registry.register_lint(&UNRESOLVED_GLOBAL); registry.register_lint(&MISSING_TYPED_DICT_KEY); registry.register_lint(&INVALID_TYPED_DICT_STATEMENT); @@ -2677,6 +2678,32 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for redundant combinations of the `ClassVar` and `Final` type qualifiers. + /// + /// ## Why is this bad? + /// An attribute that is marked `Final` in a class body is implicitly a class variable. + /// Marking it as `ClassVar` is therefore redundant. + /// + /// Note that this diagnostic is not emitted for dataclass fields, where + /// `ClassVar[Final[int]]` has a distinct meaning from `Final[int]`. + /// + /// ## Examples + /// ```python + /// from typing import ClassVar, Final + /// + /// class C: + /// x: ClassVar[Final[int]] = 1 # redundant + /// y: Final[ClassVar[int]] = 1 # redundant + /// ``` + pub(crate) static REDUNDANT_FINAL_CLASSVAR = { + summary: "detects redundant combinations of `ClassVar` and `Final`", + status: LintStatus::stable("0.0.18"), + default_level: Level::Warn, + } +} + declare_lint! { /// ## What it does /// Detects variables declared as `global` in an inner scope that have no explicit diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index f5e2c1b1fdf0e..e57e43f056409 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -2,7 +2,10 @@ use ruff_python_ast as ast; use super::{DeferredExpressionState, TypeInferenceBuilder}; use crate::place::TypeOrigin; -use crate::types::diagnostic::{INVALID_TYPE_FORM, report_invalid_arguments_to_annotated}; +use crate::types::diagnostic::{ + INVALID_TYPE_FORM, REDUNDANT_FINAL_CLASSVAR, report_invalid_arguments_to_annotated, +}; +use crate::types::infer::nearest_enclosing_class; use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, }; @@ -279,6 +282,29 @@ impl<'db> TypeInferenceBuilder<'db, '_> { PEP613Policy::Disallowed, ); + // Emit a diagnostic if ClassVar and Final are combined in a class that is + // not a dataclass, since Final already implies the semantics of ClassVar. + let classvar_and_final = match type_qualifier { + SpecialFormType::Final => type_and_qualifiers + .qualifiers + .contains(TypeQualifiers::CLASS_VAR), + SpecialFormType::ClassVar => type_and_qualifiers + .qualifiers + .contains(TypeQualifiers::FINAL), + _ => false, + }; + if classvar_and_final + && nearest_enclosing_class(self.db(), self.index, self.scope()) + .is_none_or(|class| !class.is_dataclass_like(self.db())) + && let Some(builder) = self + .context + .report_lint(&REDUNDANT_FINAL_CLASSVAR, subscript) + { + builder.into_diagnostic(format_args!( + "`Combining `ClassVar` and `Final` is redundant" + )); + } + match type_qualifier { SpecialFormType::ClassVar => { type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR); diff --git a/scripts/conformance.py b/scripts/conformance.py index 58dcc94563518..a3680619daeaf 100644 --- a/scripts/conformance.py +++ b/scripts/conformance.py @@ -472,6 +472,7 @@ def collect_ty_diagnostics( "--ignore=assert-type-unspellable-subtype", "--error=invalid-legacy-positional-parameter", "--error=deprecated", + "--error=redundant-final-classvar", "--exit-zero", *extra_search_path_args, *map(str, test_files), @@ -507,7 +508,10 @@ def group_diagnostics_by_key( for diag in chain(old, new): diag.tag = tagged_lines.get( - (diag.location.path.name, diag.location.positions.begin.line) + ( + diag.location.path.name, + diag.location.positions.begin.line, + ) ) diagnostics = [ diff --git a/ty.schema.json b/ty.schema.json index 68dde4e32dd37..cd9f861c8c7a3 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1235,6 +1235,16 @@ } ] }, + "redundant-final-classvar": { + "title": "detects redundant combinations of `ClassVar` and `Final`", + "description": "## What it does\nChecks for redundant combinations of the `ClassVar` and `Final` type qualifiers.\n\n## Why is this bad?\nAn attribute that is marked `Final` in a class body is implicitly a class variable.\nMarking it as `ClassVar` is therefore redundant.\n\nNote that this diagnostic is not emitted for dataclass fields, where\n`ClassVar[Final[int]]` has a distinct meaning from `Final[int]`.\n\n## Examples\n```python\nfrom typing import ClassVar, Final\n\nclass C:\n x: ClassVar[Final[int]] = 1 # redundant\n y: Final[ClassVar[int]] = 1 # redundant\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "static-assert-error": { "title": "Failed static assertion", "description": "## What it does\nMakes sure that the argument of `static_assert` is statically known to be true.\n\n## Why is this bad?\nA `static_assert` call represents an explicit request from the user\nfor the type checker to emit an error if the argument cannot be verified\nto evaluate to `True` in a boolean context.\n\n## Examples\n```python\nfrom ty_extensions import static_assert\n\nstatic_assert(1 + 1 == 3) # error: evaluates to `False`\n\nstatic_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness\n```", From 2d1bfe4bb47ceb0a8d15b4512896bfc47ab31c62 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 20 Feb 2026 19:21:40 +0100 Subject: [PATCH 024/261] [ty] Bump typing conformance SHA (#23451) ## Summary Pulls in two recent changes that we made to the conformance test suite. ## Test Plan Checked that this [improves conformance](https://shark.fish/typing-conformance-report/) :-) --- .github/workflows/typing_conformance.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index a302d1ca933d7..be5999b697558 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -34,7 +34,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 RUST_BACKTRACE: 1 - CONFORMANCE_SUITE_COMMIT: ffd520acd116aacbbba793c8f1f1fd51dfda83ae + CONFORMANCE_SUITE_COMMIT: 294a354344ae54feaec0924d166779b90cde501d PYTHON_VERSION: 3.12 jobs: From ba95d94b6aa5d1b5a93575a0e09264fffd0eea6d Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 20 Feb 2026 20:14:17 +0100 Subject: [PATCH 025/261] [ty] Infer `LiteralString` for `f"{literal_str_a} {literal_str_b}"` (#23346) ## Summary * The first commit modernizes the `LiteralString` test suite. * The second commit adds more precise type inference for `f"{literal_str_a} {literal_str_b}"` to make a conformance test pass. ## Test Plan Updated and new Markdown tests --- .../mdtest/annotations/literal_string.md | 88 ++++++++++--------- .../src/types/infer/builder.rs | 28 ++++-- 2 files changed, 65 insertions(+), 51 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md index 3b0aa2d26c2a5..2a38900282501 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md @@ -14,9 +14,7 @@ It can be used anywhere a type is accepted: ```py from typing_extensions import LiteralString -x: LiteralString - -def f(): +def _(x: LiteralString): reveal_type(x) # revealed: LiteralString ``` @@ -64,54 +62,60 @@ class C(LiteralString): ... # error: [invalid-base] ```py from typing_extensions import LiteralString -foo: LiteralString = "foo" -reveal_type(foo) # revealed: Literal["foo"] - -bar: LiteralString = "bar" -reveal_type(foo + bar) # revealed: Literal["foobar"] - -baz: LiteralString = "baz" -baz += foo -reveal_type(baz) # revealed: Literal["bazfoo"] - -qux = (foo, bar) -reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]] - -reveal_type(foo.join(qux)) # revealed: LiteralString - -template: LiteralString = "{}, {}" -reveal_type(template) # revealed: Literal["{}, {}"] -reveal_type(template.format(foo, bar)) # revealed: LiteralString +def _(literal_a: LiteralString, literal_b: LiteralString, a_str: str): + # Addition + reveal_type(literal_a + literal_b) # revealed: LiteralString + reveal_type(literal_a + a_str) # revealed: str + reveal_type(a_str + literal_a) # revealed: str + + # In-place addition + combined_literal = literal_a + combined_literal += literal_b + reveal_type(combined_literal) # revealed: LiteralString + combined_non_literal1 = literal_a + combined_non_literal1 += a_str + reveal_type(combined_non_literal1) # revealed: str + combined_non_literal2 = a_str + combined_non_literal2 += literal_a + reveal_type(combined_non_literal2) # revealed: str + + # Join + reveal_type(literal_a.join(("abc", "foo", literal_a, literal_b))) # revealed: LiteralString + reveal_type(a_str.join(("abc", "foo", literal_a, literal_b))) # revealed: str + reveal_type(literal_a.join(("abc", "foo", a_str))) # revealed: str + + # .format(…) + reveal_type("{}, {}".format(literal_a, literal_b)) # revealed: LiteralString + reveal_type("{}, {}".format(literal_a, a_str)) # revealed: str + + # f-string + reveal_type(f"{literal_a} {literal_b}") # revealed: LiteralString + reveal_type(f"{literal_a} {a_str}") # revealed: str + + # Repetition + reveal_type(literal_a * 10) # revealed: LiteralString ``` ### Assignability -`Literal[""]` is assignable to `LiteralString`, and `LiteralString` is assignable to `str`, but not -vice versa. +`Literal["abc"]` is assignable to `LiteralString`, and `LiteralString` is assignable to `str`, but +not vice versa. ```py from typing_extensions import Literal, LiteralString +from ty_extensions import static_assert, is_assignable_to -def _(flag: bool): - foo_1: Literal["foo"] = "foo" - bar_1: LiteralString = foo_1 # fine - - foo_2 = "foo" if flag else "bar" - reveal_type(foo_2) # revealed: Literal["foo", "bar"] - bar_2: LiteralString = foo_2 # fine +static_assert(is_assignable_to(Literal[""], LiteralString)) +static_assert(is_assignable_to(Literal["abc"], LiteralString)) +static_assert(is_assignable_to(Literal["abc", "def"], LiteralString)) - foo_3: LiteralString = "foo" * 1_000_000_000 - bar_3: str = foo_2 # fine +static_assert(not is_assignable_to(LiteralString, Literal[""])) +static_assert(not is_assignable_to(LiteralString, Literal["abc"])) +static_assert(not is_assignable_to(LiteralString, Literal["abc", "def"])) - baz_1: str = repr(object()) - qux_1: LiteralString = baz_1 # error: [invalid-assignment] +static_assert(is_assignable_to(LiteralString, str)) - baz_2: LiteralString = "baz" * 1_000_000_000 - qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment] - - baz_3 = "foo" if flag else 1 - reveal_type(baz_3) # revealed: Literal["foo", 1] - qux_3: LiteralString = baz_3 # error: [invalid-assignment] +static_assert(not is_assignable_to(str, LiteralString)) ``` ### Narrowing @@ -144,9 +148,7 @@ python-version = "3.11" ```py from typing import LiteralString -x: LiteralString = "foo" - -def f(): +def _(x: LiteralString): reveal_type(x) # revealed: LiteralString ``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index c86636f546eae..f05127b4dc1f4 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -11162,12 +11162,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { || !conversion.is_none() || format_spec.is_some() { - collector.add_expression(); + collector.add_non_literal_string_expression(); } else { - if let Some(literal) = ty.str(self.db()).as_string_literal() { + let str_ty = ty.str(self.db()); + if let Some(literal) = str_ty.as_string_literal() { collector.push_str(literal.value(self.db())); + } else if str_ty.is_literal_string() { + collector.add_literal_string_expression(); } else { - collector.add_expression(); + collector.add_non_literal_string_expression(); } } } @@ -17250,14 +17253,14 @@ fn format_import_from_module(level: u32, module: Option<&str>) -> String { #[derive(Debug)] struct StringPartsCollector { concatenated: Option, - expression: bool, + contains_non_literal_str: bool, } impl StringPartsCollector { fn new() -> Self { Self { concatenated: Some(String::new()), - expression: false, + contains_non_literal_str: false, } } @@ -17274,13 +17277,22 @@ impl StringPartsCollector { } } - fn add_expression(&mut self) { + /// Add an expression whose `__str__` return type is `LiteralString`. + /// The exact value is unknown, so we can't track the concatenated string, + /// but the result is still `LiteralString`. + fn add_literal_string_expression(&mut self) { self.concatenated = None; - self.expression = true; + } + + /// Add an expression whose `__str__` return type is not `LiteralString`. + /// The result will degrade to `str`. + fn add_non_literal_string_expression(&mut self) { + self.concatenated = None; + self.contains_non_literal_str = true; } fn string_type(self, db: &dyn Db) -> Type<'_> { - if self.expression { + if self.contains_non_literal_str { KnownClass::Str.to_instance(db) } else if let Some(concatenated) = self.concatenated { Type::string_literal(db, &concatenated) From 66defe95828a65ea41d6c565305349171f536451 Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Fri, 20 Feb 2026 22:18:25 +0100 Subject: [PATCH 026/261] [`flake8-bugbear`] Tag certain `B007` diagnostics as unnecessary (#23453) Closes #23452 ## Summary Adds `DiagnosticTag::Unnecessary` to B007 diagnostics only when certainty is Certain, so editors can dim definitely-unused loop control variables without dimming uncertain cases. ## Test Plan Added new test and manually tested in VSCode. image --- .../src/rules/flake8_bugbear/mod.rs | 36 ++++++++++++++++++- .../rules/unused_loop_control_variable.rs | 5 +++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index 3e953d2f907a2..33dbc6487f9a8 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs @@ -8,13 +8,14 @@ mod tests { use std::path::Path; use anyhow::Result; + use ruff_db::diagnostic::DiagnosticTag; use test_case::test_case; use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::test::test_path; + use crate::test::{test_path, test_snippet}; use crate::settings::types::PreviewMode; use ruff_python_ast::PythonVersion; @@ -212,4 +213,37 @@ mod tests { assert_diagnostics!(snapshot, diagnostics); Ok(()) } + + #[test] + fn b007_unnecessary_tag_only_for_certain_cases() { + let settings = LinterSettings::for_rule(Rule::UnusedLoopControlVariable); + + let certain = test_snippet( + r" +for i in range(3): + print(1) +", + &settings, + ); + assert_eq!(certain.len(), 1); + assert!( + certain[0] + .primary_tags() + .is_some_and(|tags| tags.contains(&DiagnosticTag::Unnecessary)) + ); + + let uncertain = test_snippet( + r" +for i in range(3): + print(locals()) +", + &settings, + ); + assert_eq!(uncertain.len(), 1); + assert!( + !uncertain[0] + .primary_tags() + .is_some_and(|tags| tags.contains(&DiagnosticTag::Unnecessary)) + ); + } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs index e171879fa0956..ff4fb6e23eae6 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs @@ -134,6 +134,11 @@ pub(crate) fn unused_loop_control_variable(checker: &Checker, stmt_for: &ast::St }, expr.range(), ); + + if certainty == Certainty::Certain { + diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary); + } + if let Some(rename) = rename { if certainty == Certainty::Certain { // Avoid fixing if the variable, or any future bindings to the variable, are From 630aa3ae456c5798ac4291851043b8b9a6a92ae0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:04:27 +0000 Subject: [PATCH 027/261] Update dependency ruff to v0.15.2 (#23485) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index bab9bf68bfec4..c4d6cfc0354e4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.3 -ruff==0.15.1 +ruff==0.15.2 mkdocs==1.6.1 mkdocs-material==9.7.1 mkdocs-redirects==1.2.2 From e7a7ef3ba99f4385b81abf74845f6b4364119914 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:05:18 +0000 Subject: [PATCH 028/261] Update cargo-bins/cargo-binstall action to v1.17.5 (#23482) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6b05ded1ac8cb..c91d013c18ace 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -471,7 +471,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo-binstall" - uses: cargo-bins/cargo-binstall@ec80feb9e330418e014932e5982599255eff6dbb # v1.17.4 + uses: cargo-bins/cargo-binstall@7691a5b29cda4f5499b819e5618012ba4b6c3334 # v1.17.5 - name: "Install cargo-fuzz" # Download the latest version from quick install and not the github releases because github releases only has MUSL targets. run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm @@ -724,7 +724,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@ec80feb9e330418e014932e5982599255eff6dbb # v1.17.4 + - uses: cargo-bins/cargo-binstall@7691a5b29cda4f5499b819e5618012ba4b6c3334 # v1.17.5 - run: cargo binstall --no-confirm cargo-shear - run: cargo shear From 42a48c2c3cf8d5d7555cbbf1a3987c9111fb6623 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:05:32 +0000 Subject: [PATCH 029/261] Update prek dependencies (#23486) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6934a0f6596e3..2fe74794b02b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: priority: 0 - repo: https://github.com/crate-ci/typos - rev: v1.43.3 + rev: v1.43.4 hooks: - id: typos priority: 0 @@ -93,7 +93,7 @@ repos: priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.0 + rev: v0.15.1 hooks: - id: ruff-format priority: 0 @@ -137,7 +137,7 @@ repos: # `actionlint` hook, for verifying correct syntax in GitHub Actions workflows. # Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`. - repo: https://github.com/rhysd/actionlint - rev: v1.7.10 + rev: v1.7.11 hooks: - id: actionlint stages: From 0538e45df85ae2d09eef0e1543fda4000b203e91 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:05:54 +0000 Subject: [PATCH 030/261] Update dependency astral-sh/uv to v0.10.4 (#23484) --- .github/workflows/ci.yaml | 28 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/mypy_primer.yaml | 4 +-- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/sync_typeshed.yaml | 6 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- .github/workflows/ty-ecosystem-report.yaml | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c91d013c18ace..d3c364fc6cfb7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -291,7 +291,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -350,7 +350,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -384,7 +384,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" enable-cache: "true" - name: "Run tests" run: | @@ -491,7 +491,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: shared-key: ruff-linux-debug @@ -528,7 +528,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -572,7 +572,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.10.2" + version: "0.10.4" - name: "Install Rust toolchain" run: rustup show @@ -678,7 +678,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -739,7 +739,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -792,7 +792,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 24 @@ -830,7 +830,7 @@ jobs: with: python-version: 3.13 activate-environment: true - version: "0.10.2" + version: "0.10.4" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -981,7 +981,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: "Install Rust toolchain" run: rustup show @@ -1062,7 +1062,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: "Install codspeed" uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 @@ -1113,7 +1113,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: "Install Rust toolchain" run: rustup show @@ -1157,7 +1157,7 @@ jobs: - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: "Install codspeed" uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index c73925c377603..b0dabeea5b4d1 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -36,7 +36,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index cf3b9ab115c82..6531b1dd7a252 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -54,7 +54,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: @@ -99,7 +99,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index edea6e9eecef7..90433e868017d 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -24,7 +24,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: wheels-* diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index b59cbf0c1df79..b24216f45410c 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -78,7 +78,7 @@ jobs: git config --global user.email '<>' - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -134,7 +134,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: Setup git run: | git config --global user.name typeshedbot @@ -175,7 +175,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: - version: "0.10.2" + version: "0.10.4" - name: Setup git run: | git config --global user.name typeshedbot diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 6935274562afd..e39c0a1e8eed2 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -41,7 +41,7 @@ jobs: uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true - version: "0.10.2" + version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 55bb0c46e4b51..589e87e299bf1 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -35,7 +35,7 @@ jobs: uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true - version: "0.10.2" + version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: From d4b28c597200c61c490aa31806d5f1bec4b4cb44 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:37:09 -0500 Subject: [PATCH 031/261] Update Rust crate arc-swap to v1.8.2 (#23487) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d803fcf9b978..7059bd75c14dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ded5f9a03ac8f24d1b8a25101ee812cd32cdc8c50a4c50237de2c4915850e73" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" dependencies = [ "rustversion", ] From 43b39a8204ef8c46995414454f9e2999013ef16d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:37:15 -0500 Subject: [PATCH 032/261] Update Rust crate ctrlc to v3.5.2 (#23489) --- Cargo.lock | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7059bd75c14dc..69eff66d1861f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -552,11 +552,11 @@ version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1430e4fe087fa90b9fc465ddbe00b994df4dd2c8a05f8fd5e43815bbf541b2dc" dependencies = [ - "nix", + "nix 0.30.1", "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -571,7 +571,7 @@ dependencies = [ "colored 2.2.0", "glob", "libc", - "nix", + "nix 0.30.1", "serde", "serde_json", "statrs", @@ -686,7 +686,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -932,12 +932,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix", + "nix 0.31.1", "windows-sys 0.61.0", ] @@ -1053,7 +1053,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -1145,7 +1145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1839,7 +1839,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -2206,6 +2206,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -3725,7 +3737,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -4133,7 +4145,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -5315,7 +5327,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] From e88e930e8473e1de1f8f7bea96e02483b82de93e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:37:24 -0500 Subject: [PATCH 033/261] Update Rust crate libc to v0.2.182 (#23492) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69eff66d1861f..ede53ddff5cbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1928,9 +1928,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libcst" From 65faa143420aad562842d5eac74b3b08aa623526 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:37:32 -0500 Subject: [PATCH 034/261] Update Rust crate syn to v2.0.116 (#23493) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ede53ddff5cbf..3248408e4a3bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4109,9 +4109,9 @@ checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", From 074384e24c282a4d6b6c81bd92da80e0c290d645 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:37:57 -0500 Subject: [PATCH 035/261] Update Rust crate bitflags to v2.11.0 (#23497) --- Cargo.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3248408e4a3bc..6d09a7495baa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,7 +246,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -283,9 +283,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -1062,7 +1062,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -1381,7 +1381,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "ignore", "walkdir", ] @@ -1671,7 +1671,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "inotify-sys", "libc", ] @@ -1983,7 +1983,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "redox_syscall", ] @@ -2200,7 +2200,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -2240,7 +2240,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "fsevent-sys", "inotify", "kqueue", @@ -2917,7 +2917,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2992,7 +2992,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "once_cell", "serde", "serde_derive", @@ -3008,7 +3008,7 @@ dependencies = [ "argfile", "assert_fs", "bincode", - "bitflags 2.10.0", + "bitflags 2.11.0", "cachedir", "clap", "clap_complete_command", @@ -3269,7 +3269,7 @@ version = "0.15.2" dependencies = [ "aho-corasick", "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "clap", "colored 3.1.1", "compact_str", @@ -3391,7 +3391,7 @@ name = "ruff_python_ast" version = "0.0.0" dependencies = [ "aho-corasick", - "bitflags 2.10.0", + "bitflags 2.11.0", "compact_str", "get-size2", "is-macro", @@ -3495,7 +3495,7 @@ dependencies = [ name = "ruff_python_literal" version = "0.0.0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "itertools 0.14.0", "ruff_python_ast", "unic-ucd-category", @@ -3506,7 +3506,7 @@ name = "ruff_python_parser" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "bstr", "compact_str", "datatest-stable", @@ -3533,7 +3533,7 @@ dependencies = [ name = "ruff_python_semantic" version = "0.0.0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "insta", "is-macro", "ruff_cache", @@ -3554,7 +3554,7 @@ dependencies = [ name = "ruff_python_stdlib" version = "0.0.0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "unicode-ident", ] @@ -3733,7 +3733,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -4572,7 +4572,7 @@ dependencies = [ name = "ty_ide" version = "0.0.0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "camino", "compact_str", "get-size2", @@ -4674,7 +4674,7 @@ name = "ty_python_semantic" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "bitvec", "camino", "compact_str", @@ -4729,7 +4729,7 @@ name = "ty_server" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "crossbeam", "dunce", "insta", @@ -5259,7 +5259,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -5645,7 +5645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "indexmap", "log", "serde", From 61d262938f33b4558beca71ec02439248fe2cc68 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:38:09 -0500 Subject: [PATCH 036/261] Update CodSpeedHQ/action action to v4.10.6 (#23483) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d3c364fc6cfb7..970b28bcdd67c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -995,7 +995,7 @@ jobs: run: cargo codspeed build --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser - name: "Run benchmarks" - uses: CodSpeedHQ/action@9a74b6ba663684052277e8f342b5eb99c7442a03 # v4.10.5 + uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f # v4.10.6 with: mode: simulation run: cargo codspeed run @@ -1080,7 +1080,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@9a74b6ba663684052277e8f342b5eb99c7442a03 # v4.10.5 + uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f # v4.10.6 with: mode: simulation run: cargo codspeed run --bench ty "${{ matrix.benchmark }}" @@ -1175,7 +1175,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@9a74b6ba663684052277e8f342b5eb99c7442a03 # v4.10.5 + uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f # v4.10.6 env: # enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't # appear to provide much useful insight for our walltime benchmarks right now From e306d0199942aa943fd1a9f9f77dc32ac0ae1fca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:38:27 -0500 Subject: [PATCH 037/261] Update Rust crate jiff to v0.2.20 (#23491) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d09a7495baa0..cd5a4f15565be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1829,9 +1829,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1844,9 +1844,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" dependencies = [ "proc-macro2", "quote", From 9cb61dd6ff4fe96af774ad182d303dd08febd9cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:38:46 -0500 Subject: [PATCH 038/261] Update Rust crate clap to v4.5.58 (#23488) --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd5a4f15565be..eb9aa56b581e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -477,9 +477,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -487,9 +487,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -542,9 +542,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clearscreen" From 8f2bfc3e3ea58d87e64e63e1c857faf55d4bab8e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:39:12 -0500 Subject: [PATCH 039/261] Update Rust crate indicatif to v0.18.4 (#23490) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb9aa56b581e3..e99ad2804947c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1644,9 +1644,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console 0.16.1", "portable-atomic", @@ -2607,9 +2607,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" From 250de6d17fbb71eab5710e33bbd9a6ee88fa4f38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:45:32 -0500 Subject: [PATCH 040/261] Update Rust crate quickcheck to v1.1.0 (#23498) --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e99ad2804947c..3e7eeaa0c1f17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2754,11 +2754,11 @@ dependencies = [ [[package]] name = "quickcheck" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +checksum = "95c589f335db0f6aaa168a7cd27b1fc6920f5e1470c804f814d9cd6e62a0f70b" dependencies = [ - "rand 0.8.5", + "rand 0.10.0", ] [[package]] From e49d5ac8527f3e81e134e5ffd71deef6563d3c5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:46:31 +0000 Subject: [PATCH 041/261] Update Rust crate uuid to v1.21.0 (#23501) --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e7eeaa0c1f17..512abe5075e02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5039,11 +5039,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.1", "js-sys", "rand 0.9.2", "wasm-bindgen", From 0cb0d413b1285f2655945458d5f96577bec9bd0a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:47:31 -0500 Subject: [PATCH 042/261] Update taiki-e/install-action action to v2.67.30 (#23494) --- .github/workflows/ci.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 970b28bcdd67c..c91cff06ba1c1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -281,11 +281,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-insta - name: "Install uv" @@ -344,7 +344,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-nextest - name: "Install uv" @@ -378,7 +378,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-nextest - name: "Install uv" @@ -987,7 +987,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-codspeed @@ -1026,7 +1026,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-codspeed @@ -1065,7 +1065,7 @@ jobs: version: "0.10.4" - name: "Install codspeed" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-codspeed @@ -1119,7 +1119,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-codspeed @@ -1160,7 +1160,7 @@ jobs: version: "0.10.4" - name: "Install codspeed" - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: cargo-codspeed From 1da1429f020d11796daa3ffda0f3964f15dbe009 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:47:37 -0500 Subject: [PATCH 043/261] Update actions/attest-build-provenance action to v3.2.0 (#23495) --- .github/workflows/build-docker.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 64a1614f971b8..c21a08ae2d639 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -174,7 +174,7 @@ jobs: echo "digest=${digest}" >> "$GITHUB_OUTPUT" - name: Generate artifact attestation - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-name: ${{ env.RUFF_BASE_IMG }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} @@ -279,7 +279,7 @@ jobs: annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-name: ${{ env.RUFF_BASE_IMG }} subject-digest: ${{ steps.build-and-push.outputs.digest }} @@ -358,7 +358,7 @@ jobs: echo "digest=${digest}" >> "$GITHUB_OUTPUT" - name: Generate artifact attestation - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-name: ${{ env.RUFF_BASE_IMG }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} From 32cced440ab829b32e90351c28975ad9a351a379 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:48:58 +0900 Subject: [PATCH 044/261] Fix libc version conflict (#23508) --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 512abe5075e02..3d6261b76e05a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1928,9 +1928,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libcst" @@ -2212,7 +2212,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", From 26a02583b335b69937165df0ed61a7ce745d1ee4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:27:41 +0100 Subject: [PATCH 045/261] Update Rust crate quickcheck_macros to v1.2.0 (#23499) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [quickcheck_macros](https://redirect.github.com/BurntSushi/quickcheck) | workspace.dependencies | minor | `1.1.0` → `1.2.0` | --- ### Release Notes
BurntSushi/quickcheck (quickcheck_macros) ### [`v1.2.0`](https://redirect.github.com/BurntSushi/quickcheck/compare/quickcheck_macros-1.1.0...quickcheck_macros-1.2.0) [Compare Source](https://redirect.github.com/BurntSushi/quickcheck/compare/quickcheck_macros-1.1.0...quickcheck_macros-1.2.0)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d6261b76e05a..1ad6b273764db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2763,9 +2763,9 @@ dependencies = [ [[package]] name = "quickcheck_macros" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a" +checksum = "a9a28b8493dd664c8b171dd944da82d933f7d456b829bfb236738e1fe06c5ba4" dependencies = [ "proc-macro2", "quote", From a0050421d39b83a0c516352a8c34f7913b134389 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:30:53 +0100 Subject: [PATCH 046/261] Update docker/build-push-action action to v6.19.2 (#23496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [docker/build-push-action](https://redirect.github.com/docker/build-push-action) | action | minor | `v6.18.0` → `v6.19.2` | --- ### Release Notes
docker/build-push-action (docker/build-push-action) ### [`v6.19.2`](https://redirect.github.com/docker/build-push-action/releases/tag/v6.19.2) [Compare Source](https://redirect.github.com/docker/build-push-action/compare/v6.19.1...v6.19.2) - Preserve port in `GIT_AUTH_TOKEN` host by [@​crazy-max](https://redirect.github.com/crazy-max) in [#​1458](https://redirect.github.com/docker/build-push-action/pull/1458) **Full Changelog**: ### [`v6.19.1`](https://redirect.github.com/docker/build-push-action/releases/tag/v6.19.1) [Compare Source](https://redirect.github.com/docker/build-push-action/compare/v6.19.0...v6.19.1) - Derive `GIT_AUTH_TOKEN` host from GitHub server URL by [@​crazy-max](https://redirect.github.com/crazy-max) in [#​1456](https://redirect.github.com/docker/build-push-action/pull/1456) **Full Changelog**: ### [`v6.19.0`](https://redirect.github.com/docker/build-push-action/releases/tag/v6.19.0) [Compare Source](https://redirect.github.com/docker/build-push-action/compare/v6.18.0...v6.19.0) - Scope default git auth token to `github.com` by [@​crazy-max](https://redirect.github.com/crazy-max) in [#​1451](https://redirect.github.com/docker/build-push-action/pull/1451) - Bump brace-expansion from 1.1.11 to 1.1.12 in [#​1396](https://redirect.github.com/docker/build-push-action/pull/1396) - Bump form-data from 2.5.1 to 2.5.5 in [#​1391](https://redirect.github.com/docker/build-push-action/pull/1391) - Bump js-yaml from 3.14.1 to 3.14.2 in [#​1429](https://redirect.github.com/docker/build-push-action/pull/1429) - Bump lodash from 4.17.21 to 4.17.23 in [#​1446](https://redirect.github.com/docker/build-push-action/pull/1446) - Bump tmp from 0.2.3 to 0.2.4 in [#​1398](https://redirect.github.com/docker/build-push-action/pull/1398) - Bump undici from 5.28.4 to 5.29.0 in [#​1397](https://redirect.github.com/docker/build-push-action/pull/1397) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index c21a08ae2d639..957f8d9554ad0 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -85,7 +85,7 @@ jobs: # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/ - name: Build and push by digest id: build - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . platforms: ${{ matrix.platform }} @@ -266,7 +266,7 @@ jobs: - name: Build and push id: build-and-push - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . platforms: linux/amd64,linux/arm64 From ba330dba60cc260e4559face582e236d1b6c6d7b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:46:36 +0100 Subject: [PATCH 047/261] Update Rust crate toml to v1 (#23505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | Pending | |---|---|---|---|---| | [toml](https://redirect.github.com/toml-rs/toml) | workspace.dependencies | major | `0.9.0` → `1.0.0` | `1.0.3` (+1) | --- ### Release Notes
toml-rs/toml (toml) ### [`v1.0.1`](https://redirect.github.com/toml-rs/toml/compare/toml-v1.0.0...toml-v1.0.1) [Compare Source](https://redirect.github.com/toml-rs/toml/compare/toml-v1.0.0...toml-v1.0.1) ### [`v1.0.0`](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.12...toml-v1.0.0) [Compare Source](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.12...toml-v1.0.0) ### [`v0.9.12`](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.11...toml-v0.9.12) [Compare Source](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.11...toml-v0.9.12)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 94 ++++++++++++++++++++++++++++++++++++++++++------------ Cargo.toml | 2 +- 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ad6b273764db..b74076e911de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2691,7 +2691,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", ] [[package]] @@ -2716,16 +2716,16 @@ dependencies = [ [[package]] name = "pyproject-toml" -version = "0.13.7" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d755483ad14b49e76713b52285235461a5b4f73f17612353e11a5de36a5fd2" +checksum = "7b0f6160dc48298b9260d9b958ad1d7f96f6cd0b9df200b22329204e09334663" dependencies = [ "indexmap", "pep440_rs", "pep508_rs", "serde", "thiserror 2.0.18", - "toml", + "toml 0.8.23", ] [[package]] @@ -3057,7 +3057,7 @@ dependencies = [ "test-case", "thiserror 2.0.18", "tikv-jemallocator", - "toml", + "toml 1.0.3+spec-1.1.0", "tracing", "walkdir", "wild", @@ -3073,7 +3073,7 @@ dependencies = [ "ruff_annotate_snippets", "serde", "snapbox", - "toml", + "toml 1.0.3+spec-1.1.0", "tryfn", "unicode-width", ] @@ -3195,7 +3195,7 @@ dependencies = [ "similar", "strum", "tempfile", - "toml", + "toml 1.0.3+spec-1.1.0", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -3318,7 +3318,7 @@ dependencies = [ "tempfile", "test-case", "thiserror 2.0.18", - "toml", + "toml 1.0.3+spec-1.1.0", "typed-arena", "unicode-normalization", "unicode-width", @@ -3610,7 +3610,7 @@ dependencies = [ "serde_json", "shellexpand", "thiserror 2.0.18", - "toml", + "toml 1.0.3+spec-1.1.0", "tracing", "tracing-log", "tracing-subscriber", @@ -3701,7 +3701,7 @@ dependencies = [ "shellexpand", "strum", "tempfile", - "toml", + "toml 1.0.3+spec-1.1.0", "unicode-normalization", ] @@ -3921,6 +3921,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -4339,19 +4348,40 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.0.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" dependencies = [ "indexmap", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.0.4", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "toml_writer", "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -4361,6 +4391,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.6" @@ -4368,16 +4420,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -4513,7 +4565,7 @@ dependencies = [ "serde_json", "tempfile", "tikv-jemallocator", - "toml", + "toml 1.0.3+spec-1.1.0", "tracing", "tracing-flame", "tracing-subscriber", @@ -4561,7 +4613,7 @@ dependencies = [ "ruff_text_size", "serde", "tempfile", - "toml", + "toml 1.0.3+spec-1.1.0", "ty_ide", "ty_module_resolver", "ty_project", @@ -4660,7 +4712,7 @@ dependencies = [ "serde_json", "shellexpand", "thiserror 2.0.18", - "toml", + "toml 1.0.3+spec-1.1.0", "tracing", "ty_combine", "ty_module_resolver", @@ -4817,7 +4869,7 @@ dependencies = [ "smallvec", "tempfile", "thiserror 2.0.18", - "toml", + "toml 1.0.3+spec-1.1.0", "tracing", "ty_module_resolver", "ty_python_semantic", diff --git a/Cargo.toml b/Cargo.toml index 31dbed4d25bf0..03f4ae13a43db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -188,7 +188,7 @@ tempfile = { version = "3.9.0" } test-case = { version = "3.3.1" } thiserror = { version = "2.0.0" } tikv-jemallocator = { version = "0.6.0" } -toml = { version = "0.9.0" } +toml = { version = "1.0.0" } tracing = { version = "0.1.40" } tracing-flame = { version = "0.2.0" } tracing-indicatif = { version = "0.3.11" } From b4cf6e025e22f507174c628a78ba66388f8c8caf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:47:04 +0100 Subject: [PATCH 048/261] Update Rust crate tryfn to v1 (#23506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [tryfn](https://redirect.github.com/assert-rs/snapbox) | workspace.dependencies | major | `0.2.1` → `1.0.0` | --- ### Release Notes
assert-rs/snapbox (tryfn) ### [`v1.0.0`](https://redirect.github.com/assert-rs/snapbox/compare/snapbox-v0.6.24...snapbox-v1.0.0) [Compare Source](https://redirect.github.com/assert-rs/snapbox/compare/tryfn-v0.2.3...tryfn-v1.0.0)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Charlie Marsh Co-authored-by: David Peter --- Cargo.lock | 12 ++++++------ Cargo.toml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b74076e911de6..940989dbc9fd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4025,9 +4025,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snapbox" -version = "0.6.24" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1abc378119f77310836665f8523018532cf7e3faeb3b10b01da5a7321bf8e1" +checksum = "71d70a71b68054cbe88708f77abfc4bd2daf75028f8f55f4f1cff63565df89ea" dependencies = [ "anstream", "anstyle", @@ -4045,9 +4045,9 @@ dependencies = [ [[package]] name = "snapbox-macros" -version = "0.4.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b750c344002d7cc69afb9da00ebd9b5c0f8ac2eb7d115d9d45d5b5f47718d74" +checksum = "d248cef42e1456ab2f7149c0376985351b7d849ea9ad2a957bf15ddfebf1fdf9" dependencies = [ "anstream", ] @@ -4528,9 +4528,9 @@ dependencies = [ [[package]] name = "tryfn" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe242ee9e646acec9ab73a5c540e8543ed1b107f0ce42be831e0775d423c396" +checksum = "f68b00518dd6c69ee2289900b140e55dad068cb925678603bfa8d539f61ef6c1" dependencies = [ "ignore", "libtest-mimic 0.7.3", diff --git a/Cargo.toml b/Cargo.toml index 03f4ae13a43db..1fec3e2a6c39b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,7 +173,7 @@ serde_with = { version = "3.6.0", default-features = false, features = [ shellexpand = { version = "3.0.0" } similar = { version = "2.4.0", features = ["inline"] } smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new"] } -snapbox = { version = "0.6.0", features = [ +snapbox = { version = "1.0.0", features = [ "diff", "term-svg", "cmd", @@ -199,7 +199,7 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features = "ansi", "smallvec", ] } -tryfn = { version = "0.2.1" } +tryfn = { version = "1.0.0" } typed-arena = { version = "2.0.2" } unic-ucd-category = { version = "0.9" } unicode-ident = { version = "1.0.12" } From 4001d2a36bb65d47b2167dd0ea2e5200ee843a25 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:48:19 +0100 Subject: [PATCH 049/261] Update Rust crate tempfile to v3.25.0 (#23500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [tempfile](https://stebalien.com/projects/tempfile-rs/) ([source](https://redirect.github.com/Stebalien/tempfile)) | workspace.dependencies | minor | `3.24.0` → `3.25.0` | --- ### Release Notes
Stebalien/tempfile (tempfile) ### [`v3.25.0`](https://redirect.github.com/Stebalien/tempfile/blob/HEAD/CHANGELOG.md#3250) - Allow `getrandom` 0.4.x while retaining support for `getrandom` 0.3.x.
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: David Peter --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 940989dbc9fd7..fe48327bdd477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4146,12 +4146,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.52.0", From a2231e7368cbcd4dc3b3ec67b5b9cb8f23e29b1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:50:28 +0100 Subject: [PATCH 050/261] Update Rust crate argfile to v1 (#23503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [argfile](https://redirect.github.com/rust-cli/argfile) | workspace.dependencies | major | `0.2.0` → `1.0.0` | --- ### Release Notes
rust-cli/argfile (argfile) ### [`v1.0.0`](https://redirect.github.com/rust-cli/argfile/blob/HEAD/CHANGELOG.md#100---2026-02-11) [Compare Source](https://redirect.github.com/rust-cli/argfile/compare/v0.2.1...v1.0.0)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: David Peter --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- crates/ruff/tests/integration_test.rs | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe48327bdd477..830f6c8c50710 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "argfile" -version = "0.2.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1cc0ba69de57db40674c66f7cf2caee3981ddef084388482c95c0e2133e5e8" +checksum = "99489a733dea0d2930bfa59c243146a8513ce7b0991b9d006647687cc61f53e7" dependencies = [ "fs-err", "os_str_bytes", @@ -1251,9 +1251,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "2.11.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" dependencies = [ "autocfg", ] diff --git a/Cargo.toml b/Cargo.toml index 1fec3e2a6c39b..0b77465932c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ anstream = { version = "0.6.18" } anstyle = { version = "1.0.10" } anyhow = { version = "1.0.80" } arc-swap = { version = "1.7.1" } -argfile = { version = "0.2.0" } +argfile = { version = "1.0.0" } assert_fs = { version = "1.1.0" } bincode = { version = "2.0.0" } bitflags = { version = "2.5.0" } diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 44f644011a696..1f3320811e6bc 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -1833,8 +1833,7 @@ fn missing_argfile_reports_error() { ----- stderr ----- ruff failed Cause: Failed to read CLI arguments from files - Cause: failed to open file `!.txt` - Cause: No such file or directory (os error 2) + Cause: failed to open file `!.txt`: No such file or directory (os error 2) "); }); } From cadb3c4b45cbb79a27bb09006b4848649116b8f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:08:55 +0100 Subject: [PATCH 051/261] Update Rust crate anstream to v1 (#23502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [anstream](https://redirect.github.com/rust-cli/anstyle) | workspace.dependencies | major | `0.6.18` → `1.0.0` | --- ### Release Notes
rust-cli/anstyle (anstream) ### [`v1.0.0`](https://redirect.github.com/rust-cli/anstyle/compare/anstyle-parse-v0.2.7...anstyle-parse-v1.0.0) [Compare Source](https://redirect.github.com/rust-cli/anstyle/compare/anstream-v0.6.21...anstream-v1.0.0)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: David Peter --- Cargo.lock | 38 +++++++++++++++++++++++++++++++------- Cargo.toml | 2 +- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 830f6c8c50710..14b38eedf8394 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,7 +64,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -96,6 +111,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.4" @@ -113,7 +137,7 @@ checksum = "26b9ec8c976eada1b0f9747a3d7cc4eae3bef10613e443746e7487f26c872fde" dependencies = [ "anstyle", "anstyle-lossy", - "anstyle-parse", + "anstyle-parse 0.2.7", "html-escape", "unicode-width", ] @@ -491,7 +515,7 @@ version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "clap_lex", "strsim", @@ -2006,7 +2030,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "clap", "escape8259", @@ -3067,7 +3091,7 @@ dependencies = [ name = "ruff_annotate_snippets" version = "0.1.0" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "memchr", "ruff_annotate_snippets", @@ -4029,7 +4053,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d70a71b68054cbe88708f77abfc4bd2daf75028f8f55f4f1cff63565df89ea" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "anstyle-svg", "escargot", @@ -4049,7 +4073,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d248cef42e1456ab2f7149c0376985351b7d849ea9ad2a957bf15ddfebf1fdf9" dependencies = [ - "anstream", + "anstream 0.6.21", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0b77465932c64..3648bf3457f6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ ty_test = { path = "crates/ty_test" } ty_vendored = { path = "crates/ty_vendored" } aho-corasick = { version = "1.1.3" } -anstream = { version = "0.6.18" } +anstream = { version = "1.0.0" } anstyle = { version = "1.0.10" } anyhow = { version = "1.0.80" } arc-swap = { version = "1.7.1" } From 4d468eb69f8d2360d81d45fd7387eb2040374089 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 23 Feb 2026 11:12:36 +0100 Subject: [PATCH 052/261] [ty] Bump typing conformance SHA (#23511) ## Summary Pull in a minor fix in the conformance tests: https://github.com/python/typing/compare/294a354344ae54feaec0924d166779b90cde501d...21b07859158d2d10ed7fe8d9b365412518ed9888 --- .github/workflows/typing_conformance.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index be5999b697558..97f1c8b576c57 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -34,7 +34,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 RUST_BACKTRACE: 1 - CONFORMANCE_SUITE_COMMIT: 294a354344ae54feaec0924d166779b90cde501d + CONFORMANCE_SUITE_COMMIT: 21b07859158d2d10ed7fe8d9b365412518ed9888 PYTHON_VERSION: 3.12 jobs: From fc1081afb7d2566eac1970cb110503398970fd66 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 23 Feb 2026 06:41:38 -0500 Subject: [PATCH 053/261] [ty] Detect terminal `await` calls to `async` functions (#23479) ## Summary Closes https://github.com/astral-sh/ty/issues/2789. --------- Co-authored-by: David Peter --- .../mdtest/narrow/post_if_statement.md | 14 +++++++++++ .../resources/mdtest/terminal_statements.md | 22 ++++++++++++++++++ .../src/semantic_index/builder.rs | 23 +++++++++++++++---- .../src/semantic_index/predicate.rs | 4 ++++ .../reachability_constraints.rs | 3 ++- 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/post_if_statement.md b/crates/ty_python_semantic/resources/mdtest/narrow/post_if_statement.md index 76d96d746baf1..eb93a96f1cde9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/post_if_statement.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/post_if_statement.md @@ -242,3 +242,17 @@ def bar(): # v was reassigned, so any narrowing shouldn't apply reveal_type(v) # revealed: int | None ``` + +## Narrowing preserved when `await`ing a `NoReturn` function in one branch + +```py +from typing import NoReturn + +async def stop() -> NoReturn: + raise NotImplementedError + +async def main(val: int | None): + if val is None: + await stop() + reveal_type(val) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md index 762a77746ee1f..ff883ea576f0a 100644 --- a/crates/ty_python_semantic/resources/mdtest/terminal_statements.md +++ b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md @@ -808,6 +808,28 @@ def _() -> NoReturn: C().die() ``` +### Awaiting async `NoReturn` functions + +Awaiting an async function annotated as returning `NoReturn` should be treated as terminal, just +like calling a synchronous `NoReturn` function. + +```py +from typing import NoReturn + +async def stop() -> NoReturn: + raise NotImplementedError + +async def main(flag: bool): + if flag: + x = "terminal" + await stop() + else: + x = "test" + pass + + reveal_type(x) # revealed: Literal["test"] +``` + ## Nested functions Free references inside of a function body refer to variables defined in the containing scope. diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 76cb3c3df1ef3..8cb1948a85a0b 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2708,8 +2708,9 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { self.visit_expr(value); - // If the statement is a call, it could possibly be a call to a function - // marked with `NoReturn` (for example, `sys.exit()`). In this case, we use a special + // If the statement is a call (or an `await` wrapping a call), it could + // possibly be a call to a function marked with `NoReturn` (for example, + // `sys.exit()` or `await async_exit()`). In this case, we use a special // kind of constraint to mark the following code as unreachable. // // Ideally, these constraints should be added for every call expression, even those in @@ -2721,15 +2722,29 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { // We also only add these inside function scopes, since considering module-level // constraints can affect the type of imported symbols, leading to a lot more // work in third-party code. - if let ast::Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { + let call_info = match value.as_ref() { + ast::Expr::Call(ast::ExprCall { func, .. }) => { + Some((func.as_ref(), value.as_ref(), false)) + } + ast::Expr::Await(ast::ExprAwait { value: inner, .. }) => match inner.as_ref() { + ast::Expr::Call(ast::ExprCall { func, .. }) => { + Some((func.as_ref(), value.as_ref(), true)) + } + _ => None, + }, + _ => None, + }; + + if let Some((func, expr, is_await)) = call_info { if !self.source_type.is_stub() && self.in_function_scope() { let callable = self.add_standalone_expression(func); - let call_expr = self.add_standalone_expression(value.as_ref()); + let call_expr = self.add_standalone_expression(expr); let predicate = Predicate { node: PredicateNode::ReturnsNever(CallableAndCallExpr { callable, call_expr, + is_await, }), is_positive: false, }; diff --git a/crates/ty_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs index cb0519e6ca674..528e8143385a3 100644 --- a/crates/ty_python_semantic/src/semantic_index/predicate.rs +++ b/crates/ty_python_semantic/src/semantic_index/predicate.rs @@ -102,6 +102,10 @@ impl PredicateOrLiteral<'_> { pub(crate) struct CallableAndCallExpr<'db> { pub(crate) callable: Expression<'db>, pub(crate) call_expr: Expression<'db>, + /// Whether the call is wrapped in an `await` expression. If `true`, `call_expr` refers to the + /// `await` expression rather than the call itself. This is used to detect terminal `await`s of + /// async functions that return `Never`. + pub(crate) is_await: bool, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 647bb088d1582..20baeaa42df7d 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -1070,6 +1070,7 @@ impl ReachabilityConstraints { PredicateNode::ReturnsNever(CallableAndCallExpr { callable, call_expr, + is_await, }) => { // We first infer just the type of the callable. In the most likely case that the // function is not marked with `NoReturn`, or that it always returns `NoReturn`, @@ -1111,7 +1112,7 @@ impl ReachabilityConstraints { any_overload_is_generic |= overload.return_ty.has_typevar(db); } - if no_overloads_return_never && !any_overload_is_generic { + if no_overloads_return_never && !any_overload_is_generic && !is_await { Truthiness::AlwaysFalse } else if all_overloads_return_never { Truthiness::AlwaysTrue From 7778f653833ead6a54df1f51fb3479b35385eae4 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 23 Feb 2026 13:08:51 +0000 Subject: [PATCH 054/261] [ty] Fix upcasting `type[T]` types to `Callable` types (#23472) --- .../resources/mdtest/type_of/generics.md | 95 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 80 +++++++++++----- .../src/types/signatures.rs | 5 + 3 files changed, 157 insertions(+), 23 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index b1ef6fa4631ae..757200e5350a8 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -491,3 +491,98 @@ expects_type_c_default(C[int]) expects_type_c_default_of_int(C[str]) expects_type_c_default_of_int_str(C[str, int]) ``` + +## Upcasting a `type[]` type to a `Callable` type + +`type[T]` accepts the same parameters as `object.__init__` if `T` does not have an upper bound. If +`T` is bound to a nominal-instance type, `type[T]` accepts the same parameters as the constructor of +the class that the instance-type refers to. + +```py +from ty_extensions import CallableTypeOf + +class TakesStrInConstructor: + def __init__(self, x: int, y: str | None = None): ... + +class TakesIntInConstructor: + def __init__(self, x: int, y: int | None = None): ... + +def f[ + T, + T1: object, + T2: int, + T3: TakesStrInConstructor | TakesIntInConstructor, + T4: (TakesStrInConstructor, TakesIntInConstructor), +]( + bare_type: type, + type_object: type[object], + type_t_unbound: type[T], + type_t_object_bound: type[T1], + type_int: type[int], + type_t_int_bound: type[T2], + type_t_union_bound: type[T3], + type_t_constrained: type[T4], +): + # TODO: these are all `Any` because of typeshed's signature for `type.__call__`. + # We could consider overriding that. + reveal_type(bare_type()) # revealed: Any + reveal_type(type_object()) # revealed: Any + + # TODO: we could consider emitting errors for these two, but don't currently, + # for the same reason + reveal_type(bare_type("")) # revealed: Any + reveal_type(type_object("")) # revealed: Any + + reveal_type(type_t_unbound()) # revealed: T@f + reveal_type(type_t_unbound("")) # revealed: T@f + + reveal_type(type_int()) # revealed: int + reveal_type(type_int("1")) # revealed: int + # error: [invalid-argument-type] + reveal_type(type_int([])) # revealed: int + + reveal_type(type_t_union_bound(42)) # revealed: T3@f + # error: [invalid-argument-type] + reveal_type(type_t_union_bound(42, "")) # revealed: T3@f + # error: [invalid-argument-type] + reveal_type(type_t_union_bound(42, 42)) # revealed: T3@f + + reveal_type(type_t_constrained(42)) # revealed: T4@f + # error: [invalid-argument-type] + reveal_type(type_t_constrained(42, "")) # revealed: T4@f + # error: [invalid-argument-type] + reveal_type(type_t_constrained(42, 42)) # revealed: T4@f + + def g( + object_class_upcast: CallableTypeOf[object], + bare_type_upcast: CallableTypeOf[bare_type], + type_object_upcast: CallableTypeOf[type_object], + type_t_unbound_upcast: CallableTypeOf[type_t_unbound], + type_t_object_bound_upcast: CallableTypeOf[type_t_object_bound], + type_int_upcast: CallableTypeOf[type_int], + type_t_int_bound_upcast: CallableTypeOf[type_t_int_bound], + type_t_union_bound_upcast: CallableTypeOf[type_t_union_bound], + type_t_constrained_upcast: CallableTypeOf[type_t_constrained], + ): + reveal_type(object_class_upcast) # revealed: () -> object + + # TODO: these two could arguably be `() -> object`, + # but have more dynamic signatures due to typeshed's `type.__call__` annotations. + # We could consider overriding those. + reveal_type(bare_type_upcast) # revealed: (...) -> Any + reveal_type(type_object_upcast) # revealed: (...) -> Any + + # TODO: if we did decide to override typeshed's `type.__call__` annotations (see above), + # we should also turn these two into `() -> T@f` / `() -> T1@f` + reveal_type(type_t_unbound_upcast) # revealed: (...) -> T@f + reveal_type(type_t_object_bound_upcast) # revealed: (...) -> T1@f + + # revealed: Overload[(x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> int, (x: str | bytes | bytearray, /, base: SupportsIndex) -> int] + reveal_type(type_int_upcast) + # revealed: Overload[(x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> T2@f, (x: str | bytes | bytearray, /, base: SupportsIndex) -> T2@f] + reveal_type(type_t_int_bound_upcast) + # revealed: ((x: int, y: str | None = None) -> T3@f) | ((x: int, y: int | None = None) -> T3@f) + reveal_type(type_t_union_bound_upcast) + # revealed: ((x: int, y: str | None = None) -> T4@f) | ((x: int, y: int | None = None) -> T4@f) + reveal_type(type_t_constrained_upcast) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a4d17e190a163..d7d82eed577d1 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2092,13 +2092,49 @@ impl<'db> Type<'db> { // TODO: This is unsound so in future we can consider an opt-in option to disable it. Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(class) => Some(class.into_callable(db)), - - SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => { - Some(CallableTypes::one(CallableType::single( + SubclassOfInner::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + let upcast_callables = bound.to_meta_type(db).try_upcast_to_callable(db)?; + Some(upcast_callables.map(|callable| { + let signatures = callable + .signatures(db) + .into_iter() + .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); + CallableType::new( + db, + CallableSignature::from_overloads(signatures), + callable.kind(db), + ) + })) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + let mut callables = SmallVec::new(); + for constraint in constraints.elements(db) { + let element_upcast = + constraint.to_meta_type(db).try_upcast_to_callable(db)?; + for callable in element_upcast.into_inner() { + let signatures = callable + .signatures(db) + .into_iter() + .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); + callables.push(CallableType::new( + db, + CallableSignature::from_overloads(signatures), + callable.kind(db), + )); + } + } + Some(CallableTypes(callables)) + } + None => Some(CallableTypes::one(CallableType::single( db, - Signature::new(Parameters::unknown(), Type::from(subclass_of_ty)), - ))) - } + Signature::new(Parameters::gradual_form(), Type::TypeVar(tvar)), + ))), + }, + SubclassOfInner::Dynamic(_) => Some(CallableTypes::one(CallableType::single( + db, + Signature::new(Parameters::unknown(), Type::from(subclass_of_ty)), + ))), }, Type::Union(union) => { @@ -4287,25 +4323,23 @@ impl<'db> Type<'db> { Binding::single(self, Signature::dynamic(Type::Dynamic(dynamic_type))).into() } SubclassOfInner::Class(class) => self.constructor_bindings(db, class), - SubclassOfInner::TypeVar(bound_typevar) => { - let Some(class) = (match bound_typevar.typevar(db).bound_or_constraints(db) { - None | Some(TypeVarBoundOrConstraints::UpperBound(_)) => { - subclass_of_type.subclass_of().into_class(db) + SubclassOfInner::TypeVar(tvar) => { + let bindings = match tvar.typevar(db).bound_or_constraints(db) { + None => KnownClass::Type.to_instance(db).bindings(db), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.to_meta_type(db).bindings(db) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + Bindings::from_union( + self, + constraints + .elements(db) + .iter() + .map(|ty| ty.to_meta_type(db).bindings(db)), + ) } - // TODO: model calls to `type[T]` where `T` is constrained - Some(TypeVarBoundOrConstraints::Constraints(_)) => None, - }) else { - return Binding::single( - self, - Signature::new( - Parameters::gradual_form(), - self.to_instance(db).unwrap_or(Type::unknown()), - ), - ) - .into(); }; - - self.constructor_bindings(db, class) + bindings.with_constructor_instance_type(Type::TypeVar(tvar)) } }, diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 53f7aef427f76..e837a850a7568 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1715,6 +1715,11 @@ impl<'db> Signature<'db> { pub(crate) fn with_definition(self, definition: Option>) -> Self { Self { definition, ..self } } + + /// Create a new signature with the given return type. + pub(crate) fn with_return_type(self, return_ty: Type<'db>) -> Self { + Self { return_ty, ..self } + } } impl<'db> VarianceInferable<'db> for &Signature<'db> { From e5ca9dd8f03e4cbafd5c3a0af3b55a8b865f5d4a Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:15:26 -0500 Subject: [PATCH 055/261] Update isort action comments heading (#23515) Summary -- Closes #23450 by renaming the `Action comments` heading to `isort action comments`. I agree with the issue author that this should make it easier to find. Test Plan -- --- docs/linter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/linter.md b/docs/linter.md index 35ed415705bbc..077b830003c9e 100644 --- a/docs/linter.md +++ b/docs/linter.md @@ -460,7 +460,7 @@ relevant lines (with the appropriate rule codes), run Ruff with `--add-noqa`, li $ ruff check /path/to/file.py --add-noqa ``` -### Action comments +### isort action comments Ruff respects isort's [action comments](https://pycqa.github.io/isort/docs/configuration/action_comments.html) (`# isort: skip_file`, `# isort: on`, `# isort: off`, `# isort: skip`, and `# isort: split`), which From f8d1dd297cf059003467a8d30c197b11145f2547 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:23:39 -0500 Subject: [PATCH 056/261] Render sub-diagnostics in the GitHub output format (#23455) ## Summary This PR updates our GitHub output format rendering to include sub-diagnostics and secondary annotations rather than just the concise message. As shown in the test plan, @AlexWaygood and I did some trial and error to figure out how to get these to render correctly, with some help from William on Discord too. ## Test Plan Existing snapshot tests, updated to include a diagnostic with sub-diagnostics (also reflected in some other unmodified output formats), as well as manual testing in https://github.com/ntBre/github-diagnostic-test/pull/2: image image --------- Co-authored-by: Alex Waygood --- .../cli__lint__output_format_github.snap | 2 +- crates/ruff_db/src/diagnostic/mod.rs | 38 +++++--- crates/ruff_db/src/diagnostic/render.rs | 62 ++++++------- .../ruff_db/src/diagnostic/render/concise.rs | 3 + crates/ruff_db/src/diagnostic/render/full.rs | 38 ++++++++ .../ruff_db/src/diagnostic/render/github.rs | 92 ++++++++++++++++++- crates/ruff_db/src/diagnostic/render/junit.rs | 10 +- ...gnostic__render__azure__tests__output.snap | 1 + ...nostic__render__github__tests__output.snap | 5 +- ...nostic__render__gitlab__tests__output.snap | 19 ++++ ...agnostic__render__json__tests__output.snap | 17 ++++ ...ic__render__json_lines__tests__output.snap | 1 + ...gnostic__render__junit__tests__output.snap | 7 +- ...render__junit__tests__sub_diagnostics.snap | 12 --- ...nostic__render__pylint__tests__output.snap | 1 + ...nostic__render__rdjson__tests__output.snap | 20 ++++ crates/ty/tests/cli/main.rs | 8 +- 17 files changed, 262 insertions(+), 74 deletions(-) delete mode 100644 crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__sub_diagnostics.snap diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap index 32dc44924810e..631519ad56e09 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap @@ -16,7 +16,7 @@ info: success: false exit_code: 1 ----- stdout ----- -::error title=ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused +::error title=ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused%0A help: Remove unused import: `os` ::error title=ruff (F821),file=[TMP]/input.py,line=2,col=5,endLine=2,endColumn=6::input.py:2:5: F821 Undefined name `y` ::error title=ruff (invalid-syntax),file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 70cf38e8ea47c..6f14e1aebb324 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -317,17 +317,7 @@ impl Diagnostic { /// Returns all annotations, skipping the first primary annotation. pub fn secondary_annotations(&self) -> impl Iterator { - let mut seen_primary = false; - self.inner.annotations.iter().filter(move |ann| { - if seen_primary { - true - } else if ann.is_primary { - seen_primary = true; - false - } else { - true - } - }) + secondary_annotations(self.inner.annotations.iter()) } pub fn sub_diagnostics(&self) -> &[SubDiagnostic] { @@ -669,6 +659,11 @@ impl SubDiagnostic { &self.inner.annotations } + /// Returns all annotations, skipping the first primary annotation. + pub fn secondary_annotations(&self) -> impl Iterator { + secondary_annotations(self.inner.annotations.iter()) + } + /// Returns a mutable borrow of the annotations of this sub-diagnostic. pub fn annotations_mut(&mut self) -> impl Iterator { self.inner.annotations.iter_mut() @@ -707,6 +702,10 @@ impl SubDiagnostic { ConciseMessage::Both { main, annotation } } } + + pub(crate) fn severity(&self) -> SubDiagnosticSeverity { + self.inner.severity + } } #[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)] @@ -716,6 +715,23 @@ struct SubDiagnosticInner { annotations: Vec, } +/// Returns all annotations, skipping the first primary annotation. +fn secondary_annotations<'a>( + annotations: impl Iterator, +) -> impl Iterator { + let mut seen_primary = false; + annotations.filter(move |ann| { + if seen_primary { + true + } else if ann.is_primary { + seen_primary = true; + false + } else { + true + } + }) +} + /// A pointer to a subsequence in the end user's input. /// /// Also known as an annotation, the pointer can optionally contain a short diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 1770ebcacfaea..e41763e466b7a 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -2798,6 +2798,16 @@ watermelon self } + /// Adds a sub-diagnostic constructed with this diagnostic's environment. + fn sub( + mut self, + f: impl Fn(&mut TestEnvironment) -> SubDiagnostic, + ) -> DiagnosticBuilder<'e> { + let sub = f(self.env); + self.diag.sub(sub); + self + } + /// Set the documentation URL for the diagnostic. pub(super) fn documentation_url(mut self, url: impl Into) -> DiagnosticBuilder<'e> { self.diag.set_documentation_url(Some(url.into())); @@ -2901,7 +2911,7 @@ def fibonacci(n): elif n == 1: return 1 else: - return fibonacci(n - 1) + fibonacci(n - 2) + return fibonaccii(n - 1) + fibonacci(n - 2) "#, ); env.add("undef.py", r"if a == 1: pass"); @@ -2940,6 +2950,26 @@ def fibonacci(n): .noqa_offset(TextSize::from(3)) .documentation_url("https://docs.astral.sh/ruff/rules/undefined-name") .build(), + env.builder( + "undefined-name", + Severity::Error, + "Undefined name `fibonaccii`", + ) + .primary("fib.py", "12:15", "12:25", "") + .secondary_code("F821") + .noqa_offset(ruff_text_size::TextSize::from(0)) + .documentation_url("https://docs.astral.sh/ruff/rules/undefined-name") + .secondary("fib.py", "12:35", "12:36", "") + .sub(|env| { + env.sub_builder( + SubDiagnosticSeverity::Info, + "Did you mean to import it from `/some/path/def.py`?", + ) + .primary("fib.py", "4:4", "4:13", "`fibonacci` is defined here") + .secondary("fib.py", "5:4", "5", "`fibonacci` is documented here") + .build() + }) + .build(), ]; (env, diagnostics) @@ -2973,36 +3003,6 @@ if call(foo (env, diagnostics) } - /// Create Ruff-style diagnostics with sub-diagnostics for testing the various output formats. - pub(crate) fn create_sub_diagnostics( - format: DiagnosticFormat, - ) -> (TestEnvironment, Vec) { - let mut env = TestEnvironment::new(); - env.add("/some/path/def.py", "def f(): pass"); - env.add("call.py", "f()"); - env.format(format); - - let mut primary_diagnostic = env - .builder("undefined-name", Severity::Error, "Undefined name `f`") - .primary("call.py", "1:0", "1:1", "") - .secondary_code("F821") - .noqa_offset(ruff_text_size::TextSize::from(0)) - .documentation_url("https://docs.astral.sh/ruff/rules/undefined-name") - .build(); - - let sub_diagnostic = env - .sub_builder( - SubDiagnosticSeverity::Info, - "Did you mean to import it from `/some/path/def.py`?", - ) - .primary("/some/path/def.py", "1:4", "1:5", "`f` is defined here") - .build(); - - primary_diagnostic.sub(sub_diagnostic); - - (env, vec![primary_diagnostic]) - } - /// A Jupyter notebook for testing diagnostics. /// /// diff --git a/crates/ruff_db/src/diagnostic/render/concise.rs b/crates/ruff_db/src/diagnostic/render/concise.rs index ea8e8bfaceaf3..97ae8e0ec1084 100644 --- a/crates/ruff_db/src/diagnostic/render/concise.rs +++ b/crates/ruff_db/src/diagnostic/render/concise.rs @@ -141,6 +141,7 @@ mod tests { fib.py:1:8: error[unused-import] `os` imported but unused fib.py:6:5: error[unused-variable] Local variable `x` is assigned to but never used undef.py:1:4: error[undefined-name] Undefined name `a` + fib.py:12:16: error[undefined-name] Undefined name `fibonaccii` "); } @@ -154,6 +155,7 @@ mod tests { fib.py:1:8: F401 [*] `os` imported but unused fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used undef.py:1:4: F821 Undefined name `a` + fib.py:12:16: F821 Undefined name `fibonaccii` "); } @@ -168,6 +170,7 @@ mod tests { fib.py:1:8: F401 [*] `os` imported but unused fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used undef.py:1:4: F821 Undefined name `a` + fib.py:12:16: F821 Undefined name `fibonaccii` "); } diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 986077fac17ec..ebe2de20a24fb 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -338,6 +338,25 @@ mod tests { 1 | if a == 1: pass | ^ | + + error[undefined-name]: Undefined name `fibonaccii` + --> fib.py:12:16 + | + 10 | return 1 + 11 | else: + 12 | return fibonaccii(n - 1) + fibonacci(n - 2) + | ^^^^^^^^^^ - + | + info: Did you mean to import it from `/some/path/def.py`? + --> fib.py:4:5 + | + 4 | def fibonacci(n): + | ^^^^^^^^^ `fibonacci` is defined here + 5 | """Compute the nth number in the Fibonacci sequence.""" + | ------------------------------------------------------- `fibonacci` is documented here + 6 | x = 1 + 7 | if n == 0: + | "#); } @@ -401,6 +420,25 @@ mod tests { 1 | if a == 1: pass | ^ | + + F821 Undefined name `fibonaccii` + --> fib.py:12:16 + | + 10 | return 1 + 11 | else: + 12 | return fibonaccii(n - 1) + fibonacci(n - 2) + | ^^^^^^^^^^ - + | + info: Did you mean to import it from `/some/path/def.py`? + --> fib.py:4:5 + | + 4 | def fibonacci(n): + | ^^^^^^^^^ `fibonacci` is defined here + 5 | """Compute the nth number in the Fibonacci sequence.""" + | ------------------------------------------------------- `fibonacci` is documented here + 6 | x = 1 + 7 | if n == 0: + | "#); } diff --git a/crates/ruff_db/src/diagnostic/render/github.rs b/crates/ruff_db/src/diagnostic/render/github.rs index dc4d492be331c..bba48c2ec83d5 100644 --- a/crates/ruff_db/src/diagnostic/render/github.rs +++ b/crates/ruff_db/src/diagnostic/render/github.rs @@ -1,4 +1,8 @@ -use crate::diagnostic::{Diagnostic, FileResolver, Severity}; +use ruff_text_size::TextRange; + +use crate::diagnostic::{ + Annotation, Diagnostic, FileResolver, Severity, SubDiagnosticSeverity, UnifiedFile, +}; pub(super) struct GithubRenderer<'a> { resolver: &'a dyn FileResolver, @@ -87,13 +91,97 @@ impl<'a> GithubRenderer<'a> { write!(f, "{id}:", id = diagnostic.id())?; } - writeln!(f, " {}", diagnostic.concise_message())?; + write!(f, " {}", diagnostic.concise_message())?; + + // After rendering the main diagnostic, render its secondary annotations and + // sub-diagnostics. Note that lines within a single diagnostic must be separated by + // URL-encoded newlines (`%0A`) to render properly in GitHub annotations. + for annotation in diagnostic.secondary_annotations().filter_map(|annotation| { + GithubAnnotation::from_annotation(annotation, self.resolver) + }) { + write!(f, "%0A{annotation}")?; + } + + for subdiagnostic in diagnostic.sub_diagnostics() { + let severity = match subdiagnostic.severity() { + SubDiagnosticSeverity::Help => "help", + SubDiagnosticSeverity::Info => "info", + SubDiagnosticSeverity::Warning => "warning", + SubDiagnosticSeverity::Error | SubDiagnosticSeverity::Fatal => "error", + }; + if let Some(annotation) = subdiagnostic.primary_annotation() + && let span = annotation.get_span() + && let file = span.file() + && let Some(range) = span.range() + { + let diagnostic_source = file.diagnostic_source(self.resolver); + let source_code = diagnostic_source.as_source_code(); + let message = subdiagnostic.concise_message(); + let start_location = source_code.line_column(range.start()); + write!( + f, + "%0A {path}:{row}:{column}: {severity}: {message}", + path = file.relative_path(self.resolver).display(), + row = start_location.line, + column = start_location.column, + )?; + } else { + write!(f, "%0A {severity}: {}", subdiagnostic.concise_message())?; + } + + for annotation in subdiagnostic + .secondary_annotations() + .filter_map(|annotation| { + GithubAnnotation::from_annotation(annotation, self.resolver) + }) + { + write!(f, "%0A {annotation}")?; + } + } + + writeln!(f)?; } Ok(()) } } +struct GithubAnnotation<'a> { + message: &'a str, + range: TextRange, + file: &'a UnifiedFile, + resolver: &'a dyn FileResolver, +} + +impl<'a> GithubAnnotation<'a> { + fn from_annotation(annotation: &'a Annotation, resolver: &'a dyn FileResolver) -> Option { + let span = annotation.get_span(); + Some(Self { + message: annotation.get_message()?, + range: span.range()?, + file: span.file(), + resolver, + }) + } +} + +impl std::fmt::Display for GithubAnnotation<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let diagnostic_source = self.file.diagnostic_source(self.resolver); + let source_code = diagnostic_source.as_source_code(); + let start_location = source_code.line_column(self.range.start()); + write!( + f, + " {path}:{row}:{column}:", + path = self.file.relative_path(self.resolver).display(), + row = start_location.line, + column = start_location.column, + )?; + + write!(f, " {message}", message = self.message) + } +} + #[cfg(test)] mod tests { use crate::diagnostic::{ diff --git a/crates/ruff_db/src/diagnostic/render/junit.rs b/crates/ruff_db/src/diagnostic/render/junit.rs index 1b964aafc9c5f..c24947bbd1218 100644 --- a/crates/ruff_db/src/diagnostic/render/junit.rs +++ b/crates/ruff_db/src/diagnostic/render/junit.rs @@ -184,9 +184,7 @@ impl std::io::Write for FmtAdapter<'_> { mod tests { use crate::diagnostic::{ DiagnosticFormat, - render::tests::{ - create_diagnostics, create_sub_diagnostics, create_syntax_error_diagnostics, - }, + render::tests::{create_diagnostics, create_syntax_error_diagnostics}, }; #[test] @@ -195,12 +193,6 @@ mod tests { insta::assert_snapshot!(env.render_diagnostics(&diagnostics)); } - #[test] - fn sub_diagnostics() { - let (env, diagnostics) = create_sub_diagnostics(DiagnosticFormat::Junit); - insta::assert_snapshot!(env.render_diagnostics(&diagnostics)); - } - #[test] fn syntax_errors() { let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Junit); diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__azure__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__azure__tests__output.snap index c4b0a974118d2..dd4ab6aa3dd5b 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__azure__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__azure__tests__output.snap @@ -5,3 +5,4 @@ expression: env.render_diagnostics(&diagnostics) ##vso[task.logissue type=error;sourcepath=/fib.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused ##vso[task.logissue type=error;sourcepath=/fib.py;linenumber=6;columnnumber=5;code=F841;]Local variable `x` is assigned to but never used ##vso[task.logissue type=error;sourcepath=/undef.py;linenumber=1;columnnumber=4;code=F821;]Undefined name `a` +##vso[task.logissue type=error;sourcepath=/fib.py;linenumber=12;columnnumber=16;code=F821;]Undefined name `fibonaccii` diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__output.snap index 9ec5483c55efd..d0c6ea9e159c7 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__output.snap @@ -2,6 +2,7 @@ source: crates/ruff_db/src/diagnostic/render/github.rs expression: env.render_diagnostics(&diagnostics) --- -::error title=ty (F401),file=/fib.py,line=1,col=8,endLine=1,endColumn=10::fib.py:1:8: F401 `os` imported but unused -::error title=ty (F841),file=/fib.py,line=6,col=5,endLine=6,endColumn=6::fib.py:6:5: F841 Local variable `x` is assigned to but never used +::error title=ty (F401),file=/fib.py,line=1,col=8,endLine=1,endColumn=10::fib.py:1:8: F401 `os` imported but unused%0A help: Remove unused import: `os` +::error title=ty (F841),file=/fib.py,line=6,col=5,endLine=6,endColumn=6::fib.py:6:5: F841 Local variable `x` is assigned to but never used%0A help: Remove assignment to unused variable `x` ::error title=ty (F821),file=/undef.py,line=1,col=4,endLine=1,endColumn=5::undef.py:1:4: F821 Undefined name `a` +::error title=ty (F821),file=/fib.py,line=12,col=16,endLine=12,endColumn=26::fib.py:12:16: F821 Undefined name `fibonaccii`%0A fib.py:4:5: info: Did you mean to import it from `/some/path/def.py`?: `fibonacci` is defined here%0A fib.py:5:5: `fibonacci` is documented here diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__gitlab__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__gitlab__tests__output.snap index a50787f92833e..47ff72bd44c1d 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__gitlab__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__gitlab__tests__output.snap @@ -59,5 +59,24 @@ expression: env.render_diagnostics(&diagnostics) } } } + }, + { + "check_name": "F821", + "description": "F821: Undefined name `fibonaccii`", + "severity": "major", + "fingerprint": "", + "location": { + "path": "fib.py", + "positions": { + "begin": { + "line": 12, + "column": 16 + }, + "end": { + "line": 12, + "column": 26 + } + } + } } ] diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__json__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__json__tests__output.snap index 3ede6ee332f72..79fea8b21074f 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__json__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__json__tests__output.snap @@ -85,5 +85,22 @@ expression: env.render_diagnostics(&diagnostics) "message": "Undefined name `a`", "noqa_row": 1, "url": "https://docs.astral.sh/ruff/rules/undefined-name" + }, + { + "cell": null, + "code": "F821", + "end_location": { + "column": 26, + "row": 12 + }, + "filename": "/fib.py", + "fix": null, + "location": { + "column": 16, + "row": 12 + }, + "message": "Undefined name `fibonaccii`", + "noqa_row": 1, + "url": "https://docs.astral.sh/ruff/rules/undefined-name" } ] diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__json_lines__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__json_lines__tests__output.snap index 8649d8bcb1773..5623ed9ec25a3 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__json_lines__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__json_lines__tests__output.snap @@ -5,3 +5,4 @@ expression: env.render_diagnostics(&diagnostics) {"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"/fib.py","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"} {"cell":null,"code":"F841","end_location":{"column":6,"row":6},"filename":"/fib.py","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6,"url":"https://docs.astral.sh/ruff/rules/unused-variable"} {"cell":null,"code":"F821","end_location":{"column":5,"row":1},"filename":"/undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/undefined-name"} +{"cell":null,"code":"F821","end_location":{"column":26,"row":12},"filename":"/fib.py","fix":null,"location":{"column":16,"row":12},"message":"Undefined name `fibonaccii`","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/undefined-name"} diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__output.snap index 767b620fb2b63..575975d1d5ca0 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__output.snap @@ -3,14 +3,17 @@ source: crates/ruff_db/src/diagnostic/render/junit.rs expression: env.render_diagnostics(&diagnostics) --- - - + + line 1, col 8, `os` imported but unused line 6, col 5, Local variable `x` is assigned to but never used + + line 12, col 16, Undefined name `fibonaccii` + diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__sub_diagnostics.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__sub_diagnostics.snap deleted file mode 100644 index 7e78c35ed4c47..0000000000000 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__sub_diagnostics.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/ruff_db/src/diagnostic/render/junit.rs -expression: env.render_diagnostics(&diagnostics) ---- - - - - - line 1, col 1, Undefined name `f` - - - diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__pylint__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__pylint__tests__output.snap index 5d51a37c40601..94251302974ba 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__pylint__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__pylint__tests__output.snap @@ -5,3 +5,4 @@ expression: env.render_diagnostics(&diagnostics) fib.py:1: [F401] `os` imported but unused fib.py:6: [F841] Local variable `x` is assigned to but never used undef.py:1: [F821] Undefined name `a` +fib.py:12: [F821] Undefined name `fibonaccii` diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__rdjson__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__rdjson__tests__output.snap index b20cf986d192b..fb230732b78ec 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__rdjson__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__rdjson__tests__output.snap @@ -93,6 +93,26 @@ expression: env.render_diagnostics(&diagnostics) } }, "message": "Undefined name `a`" + }, + { + "code": { + "url": "https://docs.astral.sh/ruff/rules/undefined-name", + "value": "F821" + }, + "location": { + "path": "/fib.py", + "range": { + "end": { + "column": 26, + "line": 12 + }, + "start": { + "column": 16, + "line": 12 + } + } + }, + "message": "Undefined name `fibonaccii`" } ], "severity": "WARNING", diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs index f1c1e84cae980..a741f4ef29897 100644 --- a/crates/ty/tests/cli/main.rs +++ b/crates/ty/tests/cli/main.rs @@ -109,8 +109,8 @@ fn test_output_format_env() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - ::warning title=ty (unresolved-reference),file=/test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined - ::error title=ty (not-subscriptable),file=/test.py,line=3,col=7,endLine=3,endColumn=11::test.py:3:7: not-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + ::warning title=ty (unresolved-reference),file=/test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined%0A info: rule `unresolved-reference` was selected on the command line + ::error title=ty (not-subscriptable),file=/test.py,line=3,col=7,endLine=3,endColumn=11::test.py:3:7: not-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method%0A info: rule `not-subscriptable` is enabled by default ::notice title=ty (revealed-type),file=/test.py,line=5,col=13,endLine=5,endColumn=26::test.py:5:13: revealed-type: Revealed type: `LiteralString` ----- stderr ----- @@ -794,8 +794,8 @@ fn github_diagnostics() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - ::warning title=ty (unresolved-reference),file=/test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined - ::error title=ty (not-subscriptable),file=/test.py,line=3,col=7,endLine=3,endColumn=11::test.py:3:7: not-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + ::warning title=ty (unresolved-reference),file=/test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined%0A info: rule `unresolved-reference` was selected on the command line + ::error title=ty (not-subscriptable),file=/test.py,line=3,col=7,endLine=3,endColumn=11::test.py:3:7: not-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method%0A info: rule `not-subscriptable` is enabled by default ::notice title=ty (revealed-type),file=/test.py,line=5,col=13,endLine=5,endColumn=26::test.py:5:13: revealed-type: Revealed type: `LiteralString` ----- stderr ----- From e462c03f03b3a91c664793961d90180299117209 Mon Sep 17 00:00:00 2001 From: kar-ganap Date: Mon, 23 Feb 2026 07:50:32 -0800 Subject: [PATCH 057/261] Fix F811 false positive for overloaded functions from typing-modules (#23357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #19632. When `@overload` is imported from a custom module listed in `typing-modules` (e.g., `from std import overload`), Ruff fails to recognize it as the `typing.overload` decorator and emits false F811 diagnostics for each overloaded function definition. The root cause is in `SemanticModel::match_typing_qualified_name`: `QualifiedName::from_dotted_name` splits on dots, so a single-segment module like `std` still works, but the internal representation stores it differently from what `QualifiedName::user_defined` produces. Use `user_defined` instead to match the internal representation consistently. ## Test plan - Reproduction case: `ruff check --select F811 --config ruff.toml temp.py` no longer emits false positives for `@overload` from custom typing-modules - `cargo test -p ruff_linter -- pyflakes` — all 462 tests pass --------- Co-authored-by: Brent Westbrook --- .../test/fixtures/pyflakes/F811_33.py | 19 +++++++++++++++++++ crates/ruff_linter/src/rules/pyflakes/mod.rs | 13 +++++++++++++ ...__tests__f811_typing_modules_overload.snap | 4 ++++ crates/ruff_python_semantic/src/model.rs | 2 +- 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.py create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f811_typing_modules_overload.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.py new file mode 100644 index 0000000000000..784ed8f074743 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.py @@ -0,0 +1,19 @@ +# Regression test for https://github.com/astral-sh/ruff/issues/19632 +# When @overload is imported from a custom module listed in typing-modules, +# Ruff should recognize it as typing.overload and not emit false F811 +# diagnostics for each overloaded function definition. +# +# Requires: typing-modules = ["std"] +from std import overload + + +@overload +def func(a: str, b: int) -> int: ... + + +@overload +def func(a: int, b: str) -> int: ... + + +def func(a: int | str, b: int | str) -> int: + return 0 diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index a9d5dd6d02f5d..da594769bc5f3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -708,6 +708,19 @@ mod tests { Ok(()) } + #[test] + fn f811_typing_modules_overload() -> Result<()> { + let diagnostics = test_path( + Path::new("pyflakes/F811_33.py"), + &LinterSettings { + typing_modules: vec!["std".to_string()], + ..LinterSettings::for_rule(Rule::RedefinedWhileUnused) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + #[test] fn extend_generics() -> Result<()> { let snapshot = "extend_immutable_calls".to_string(); diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f811_typing_modules_overload.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f811_typing_modules_overload.snap new file mode 100644 index 0000000000000..d0b409f39ee0b --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f811_typing_modules_overload.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 17d2a26fa49a5..ec0749f7206c2 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -209,7 +209,7 @@ impl<'a> SemanticModel<'a> { } if self.typing_modules.iter().any(|module| { - let module = QualifiedName::from_dotted_name(module); + let module = QualifiedName::user_defined(module); qualified_name == &module.append_member(target) }) { return true; From fe0950407fc86eba50331b9e01d3a06104aa7d22 Mon Sep 17 00:00:00 2001 From: Karthik Date: Mon, 23 Feb 2026 21:30:59 +0530 Subject: [PATCH 058/261] `[flake8-bandit]` Don't flag `BaseLoader`/`CBaseLoader` as unsafe in preview (`S506`) (#23510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `yaml.BaseLoader` and `yaml.CBaseLoader` use `BaseConstructor` + `BaseResolver`, which do not resolve tags or construct arbitrary Python objects — they only produce basic types (lists, dicts, strings). They are strictly *less capable* than `SafeLoader`, not less safe. Currently, S506 flags `yaml.load(..., Loader=yaml.BaseLoader)` as unsafe. This is a false positive. This PR adds `BaseLoader` and `CBaseLoader` (the C-accelerated variant from `yaml.cyaml`) to the list of recognized safe loaders, gated behind preview mode per maintainer guidance. Closes #13604 ## Test Plan - Added test fixtures covering `BaseLoader`/`CBaseLoader` via all import paths (`yaml.*`, `yaml.loader.*`, `yaml.cyaml.*`, aliased imports) - Added preview diff test case using `assert_diagnostics_diff!` to verify BaseLoader diagnostics are suppressed only in preview mode - Verified with `cargo test -p ruff_linter`, `cargo clippy`, and `uvx prek run -a` --------- Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../test/fixtures/flake8_bandit/S506.py | 12 +++ crates/ruff_linter/src/preview.rs | 5 + .../src/rules/flake8_bandit/mod.rs | 1 + .../flake8_bandit/rules/unsafe_yaml_load.rs | 9 +- ...s__flake8_bandit__tests__S506_S506.py.snap | 74 +++++++++++++++ ..._bandit__tests__preview__S506_S506.py.snap | 91 +++++++++++++++++++ 6 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S506_S506.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S506.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S506.py index b316ca8aea6ba..6726aba603861 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S506.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S506.py @@ -36,3 +36,15 @@ def test_json_load(): yaml.load("{}", Loader=yaml.CSafeLoader) yaml.load("{}", Loader=yaml.cyaml.CSafeLoader) yaml.load("{}", Loader=NewSafeLoader) + +# no issue should be found (preview mode only) +yaml.load("{}", Loader=yaml.BaseLoader) +yaml.load("{}", Loader=yaml.CBaseLoader) +yaml.load("{}", yaml.BaseLoader) +yaml.load("{}", yaml.CBaseLoader) +from yaml import BaseLoader +yaml.load("{}", Loader=BaseLoader) +from yaml.loader import BaseLoader as BL +yaml.load("{}", Loader=BL) +from yaml.cyaml import CBaseLoader +yaml.load("{}", Loader=CBaseLoader) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 893672876bb7b..05d4159a3d90a 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -292,3 +292,8 @@ pub(crate) const fn is_plural_ngettext_check_enabled(settings: &LinterSettings) pub(crate) const fn is_resolve_string_annotation_pyi041_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/23510 +pub(crate) const fn is_baseloader_safe_in_yaml_load_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs index 8616052576b7b..23ac58a41396f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs @@ -106,6 +106,7 @@ mod tests { #[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))] #[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))] #[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))] + #[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs index bc5e846f99ef8..188670c2fb6ca 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs @@ -4,6 +4,7 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; +use crate::preview::is_baseloader_safe_in_yaml_load_enabled; /// ## What it does /// Checks for uses of the `yaml.load` function. @@ -76,7 +77,13 @@ pub(crate) fn unsafe_yaml_load(checker: &Checker, call: &ast::ExprCall) { ["yaml", "SafeLoader" | "CSafeLoader"] | ["yaml", "loader", "SafeLoader" | "CSafeLoader"] | ["yaml", "cyaml", "CSafeLoader"] - ) + ) || (is_baseloader_safe_in_yaml_load_enabled(checker.settings()) + && matches!( + qualified_name.segments(), + ["yaml", "BaseLoader" | "CBaseLoader"] + | ["yaml", "loader", "BaseLoader" | "CBaseLoader"] + | ["yaml", "cyaml", "CBaseLoader"] + )) }) { let loader = match loader_arg { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S506_S506.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S506_S506.py.snap index 5e77be8256b15..302026e370c45 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S506_S506.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S506_S506.py.snap @@ -20,3 +20,77 @@ S506 Probable use of unsafe loader `Loader` with `yaml.load`. Allows instantiati 25 | 26 | # no issue should be found | + +S506 Probable use of unsafe loader `BaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:41:24 + | +40 | # no issue should be found (preview mode only) +41 | yaml.load("{}", Loader=yaml.BaseLoader) + | ^^^^^^^^^^^^^^^ +42 | yaml.load("{}", Loader=yaml.CBaseLoader) +43 | yaml.load("{}", yaml.BaseLoader) + | + +S506 Probable use of unsafe loader `CBaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:42:24 + | +40 | # no issue should be found (preview mode only) +41 | yaml.load("{}", Loader=yaml.BaseLoader) +42 | yaml.load("{}", Loader=yaml.CBaseLoader) + | ^^^^^^^^^^^^^^^^ +43 | yaml.load("{}", yaml.BaseLoader) +44 | yaml.load("{}", yaml.CBaseLoader) + | + +S506 Probable use of unsafe loader `BaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:43:17 + | +41 | yaml.load("{}", Loader=yaml.BaseLoader) +42 | yaml.load("{}", Loader=yaml.CBaseLoader) +43 | yaml.load("{}", yaml.BaseLoader) + | ^^^^^^^^^^^^^^^ +44 | yaml.load("{}", yaml.CBaseLoader) +45 | from yaml import BaseLoader + | + +S506 Probable use of unsafe loader `CBaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:44:17 + | +42 | yaml.load("{}", Loader=yaml.CBaseLoader) +43 | yaml.load("{}", yaml.BaseLoader) +44 | yaml.load("{}", yaml.CBaseLoader) + | ^^^^^^^^^^^^^^^^ +45 | from yaml import BaseLoader +46 | yaml.load("{}", Loader=BaseLoader) + | + +S506 Probable use of unsafe loader `BaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:46:24 + | +44 | yaml.load("{}", yaml.CBaseLoader) +45 | from yaml import BaseLoader +46 | yaml.load("{}", Loader=BaseLoader) + | ^^^^^^^^^^ +47 | from yaml.loader import BaseLoader as BL +48 | yaml.load("{}", Loader=BL) + | + +S506 Probable use of unsafe loader `BL` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:48:24 + | +46 | yaml.load("{}", Loader=BaseLoader) +47 | from yaml.loader import BaseLoader as BL +48 | yaml.load("{}", Loader=BL) + | ^^ +49 | from yaml.cyaml import CBaseLoader +50 | yaml.load("{}", Loader=CBaseLoader) + | + +S506 Probable use of unsafe loader `CBaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:50:24 + | +48 | yaml.load("{}", Loader=BL) +49 | from yaml.cyaml import CBaseLoader +50 | yaml.load("{}", Loader=CBaseLoader) + | ^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S506_S506.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S506_S506.py.snap new file mode 100644 index 0000000000000..1437a7b6e3295 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S506_S506.py.snap @@ -0,0 +1,91 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs +--- +--- Linter settings --- +-linter.preview = disabled ++linter.preview = enabled + +--- Summary --- +Removed: 7 +Added: 0 + +--- Removed --- +S506 Probable use of unsafe loader `BaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:41:24 + | +40 | # no issue should be found (preview mode only) +41 | yaml.load("{}", Loader=yaml.BaseLoader) + | ^^^^^^^^^^^^^^^ +42 | yaml.load("{}", Loader=yaml.CBaseLoader) +43 | yaml.load("{}", yaml.BaseLoader) + | + + +S506 Probable use of unsafe loader `CBaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:42:24 + | +40 | # no issue should be found (preview mode only) +41 | yaml.load("{}", Loader=yaml.BaseLoader) +42 | yaml.load("{}", Loader=yaml.CBaseLoader) + | ^^^^^^^^^^^^^^^^ +43 | yaml.load("{}", yaml.BaseLoader) +44 | yaml.load("{}", yaml.CBaseLoader) + | + + +S506 Probable use of unsafe loader `BaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:43:17 + | +41 | yaml.load("{}", Loader=yaml.BaseLoader) +42 | yaml.load("{}", Loader=yaml.CBaseLoader) +43 | yaml.load("{}", yaml.BaseLoader) + | ^^^^^^^^^^^^^^^ +44 | yaml.load("{}", yaml.CBaseLoader) +45 | from yaml import BaseLoader + | + + +S506 Probable use of unsafe loader `CBaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:44:17 + | +42 | yaml.load("{}", Loader=yaml.CBaseLoader) +43 | yaml.load("{}", yaml.BaseLoader) +44 | yaml.load("{}", yaml.CBaseLoader) + | ^^^^^^^^^^^^^^^^ +45 | from yaml import BaseLoader +46 | yaml.load("{}", Loader=BaseLoader) + | + + +S506 Probable use of unsafe loader `BaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:46:24 + | +44 | yaml.load("{}", yaml.CBaseLoader) +45 | from yaml import BaseLoader +46 | yaml.load("{}", Loader=BaseLoader) + | ^^^^^^^^^^ +47 | from yaml.loader import BaseLoader as BL +48 | yaml.load("{}", Loader=BL) + | + + +S506 Probable use of unsafe loader `BL` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:48:24 + | +46 | yaml.load("{}", Loader=BaseLoader) +47 | from yaml.loader import BaseLoader as BL +48 | yaml.load("{}", Loader=BL) + | ^^ +49 | from yaml.cyaml import CBaseLoader +50 | yaml.load("{}", Loader=CBaseLoader) + | + + +S506 Probable use of unsafe loader `CBaseLoader` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. + --> S506.py:50:24 + | +48 | yaml.load("{}", Loader=BL) +49 | from yaml.cyaml import CBaseLoader +50 | yaml.load("{}", Loader=CBaseLoader) + | ^^^^^^^^^^^ + | From 8591a38d53f00abf40d74207782227eb881ad8b6 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:49:35 -0500 Subject: [PATCH 059/261] Fix missing settings links for several linters (#23519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary -- Fixes #23518 by normalizing the linter names before looking them up in `Options::metadata()`. Most linters were already lowercase, matching their corresponding options, but several were not: ```console ❯ ruff linter | awk '$2 ~ /^[A-Z]/' AIR Airflow FAST FastAPI NPY NumPy-specific rules PERF Perflint F Pyflakes PL Pylint RUF Ruff-specific rules ``` Of these, only Pyflakes, Pylint, and Ruff have settings. Test Plan -- Built the docs locally: image image image --- crates/ruff_dev/src/generate_rules_table.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 1f6f890076710..5205d612eb2de 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -171,12 +171,14 @@ pub(crate) fn generate() -> String { table_out.push('\n'); } - if Options::metadata().has(&format!("lint.{}", linter.name())) { + // The linter names for Ruff and NumPy are suffixed with "-specific rules." + let linter_name = linter.name().trim_end_matches("-specific rules"); + // Several linter names are capitalized, but their settings are not. + let linter_name_lower = linter_name.to_lowercase(); + if Options::metadata().has(&format!("lint.{linter_name_lower}")) { let _ = write!( table_out, - "For related settings, see [{}](settings.md#lint{}).", - linter.name(), - linter.name(), + "For related settings, see [{linter_name}](settings.md#lint{linter_name_lower}).", ); table_out.push('\n'); table_out.push('\n'); From 1e33c4e1ebc41f9a14f41b35c9181e6bf91ce0bc Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Mon, 23 Feb 2026 11:18:47 -0800 Subject: [PATCH 060/261] [ty]: special-case comparisons of `Generator` prior to Python 3.13 (#23386) Fixes https://github.com/astral-sh/ty/issues/2426. --- .../resources/mdtest/async.md | 3 +- .../resources/mdtest/protocols.md | 54 +++++++++++++++++++ .../ty_python_semantic/src/types/instance.rs | 35 ++++++++++-- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/async.md b/crates/ty_python_semantic/resources/mdtest/async.md index 88fe643d8d5bb..f8171110f0f17 100644 --- a/crates/ty_python_semantic/resources/mdtest/async.md +++ b/crates/ty_python_semantic/resources/mdtest/async.md @@ -265,6 +265,5 @@ class B: ... async def test(x: Intersection[Coroutine[object, object, A], Coroutine[object, object, B]]): y = await x - # TODO: should be `A & B`, but suffers from https://github.com/astral-sh/ty/issues/2426 - reveal_type(y) # revealed: A + reveal_type(y) # revealed: A & B ``` diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index c5285d4821aab..f0655ba1c12b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -3394,6 +3394,60 @@ def g(x: B2[int]): pass ``` +## The `Generator` protocol's `_ReturnT_co` needs special casing prior to Python 3.13 + +Prior to Python 3.13, the `_ReturnT_co` type parameter doesn't appear in any `Generator` methods in +typeshed (except `__iter__`, which returns the self type recursively and gets normalized to `Any`). +We need to special-case `Generator` so that specializations that differ in their return type don't +appear equivalent. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from ty_extensions import is_equivalent_to, is_subtype_of, static_assert +from typing import Generator + +class A: ... +class B: ... + +static_assert(not is_equivalent_to(Generator[None, None, A], Generator[None, None, B])) +static_assert(not is_subtype_of(Generator[None, None, A], Generator[None, None, B])) +static_assert(not is_subtype_of(Generator[None, None, B], Generator[None, None, A])) + +static_assert(is_equivalent_to(Generator[None, None, A], Generator[None, None, A])) +static_assert(is_subtype_of(Generator[None, None, A], Generator[None, None, A])) +static_assert(is_subtype_of(Generator[None, None, A], Generator[None, None, A])) +``` + +## The `Generator` protocol's `_ReturnT_co` does not need special casing as of Python 3.13 + +The same test cases as above, but for Python 3.13 instead of 3.12. In this version `_ReturnT_co` +appears in `Generator`'s `close` method, and no special case is needed. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from ty_extensions import is_equivalent_to, is_subtype_of, static_assert +from typing import Generator + +class A: ... +class B: ... + +static_assert(not is_equivalent_to(Generator[None, None, A], Generator[None, None, B])) +static_assert(not is_subtype_of(Generator[None, None, A], Generator[None, None, B])) +static_assert(not is_subtype_of(Generator[None, None, B], Generator[None, None, A])) + +static_assert(is_equivalent_to(Generator[None, None, A], Generator[None, None, A])) +static_assert(is_subtype_of(Generator[None, None, A], Generator[None, None, A])) +static_assert(is_subtype_of(Generator[None, None, A], Generator[None, None, A])) +``` + ## TODO Add tests for: diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index ed58b21748d6b..266827d2d9e37 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use std::marker::PhantomData; +use ruff_python_ast::PythonVersion; use ruff_python_ast::name::Name; use ty_module_resolver::{ModuleName, file_to_module}; @@ -22,7 +23,7 @@ use crate::types::{ ApplyTypeMappingVisitor, ClassBase, ClassLiteral, FindLegacyTypeVarsVisitor, LiteralValueTypeKind, NormalizedVisitor, TypeContext, TypeMapping, VarianceInferable, }; -use crate::{Db, FxOrderSet}; +use crate::{Db, FxOrderSet, Program}; pub(super) use synthesized_protocol::SynthesizedProtocolType; impl<'db> Type<'db> { @@ -182,6 +183,20 @@ impl<'db> Type<'db> { } } + // `Generator` special case: Prior to 3.13, the `_ReturnT_co` type didn't appear in any + // methods (except `__iter__`, but that returns the self type recursively, which gets + // normalized to `Any`). We don't want generators with different return types to be + // assignable to each other. In this case we use the result of the nominal check above. + if let Some(self_protocol) = self.as_protocol_instance() + && let Protocol::FromClass(self_class) = self_protocol.inner + && let Protocol::FromClass(proto_class) = protocol.inner + && self_class.known(db) == Some(KnownClass::Generator) + && proto_class.known(db) == Some(KnownClass::Generator) + && Program::get(db).python_version(db) < PythonVersion::PY313 + { + return result; + } + let structurally_satisfied = if let Type::ProtocolInstance(self_protocol) = self { self_protocol.interface(db).has_relation_to_impl( db, @@ -803,12 +818,26 @@ impl<'db> ProtocolInstanceType<'db> { self, db: &'db dyn Db, other: Self, - _inferable: InferableTypeVars<'_, 'db>, - _visitor: &IsEquivalentVisitor<'db>, + inferable: InferableTypeVars<'_, 'db>, + visitor: &IsEquivalentVisitor<'db>, ) -> ConstraintSet<'db> { if self == other { return ConstraintSet::from(true); } + + // `Generator` special case: Prior to 3.13, the `_ReturnT_co` type didn't appear in any + // methods (except `__iter__`, but that returns the self type recursively, which gets + // normalized to `Any`). We don't want generators with different return types to be + // equivalent to each other. In this case we compare the `ClassType`s nominally. + if let Protocol::FromClass(self_class) = self.inner + && let Protocol::FromClass(other_class) = other.inner + && self_class.known(db) == Some(KnownClass::Generator) + && other_class.known(db) == Some(KnownClass::Generator) + && Program::get(db).python_version(db) < PythonVersion::PY313 + { + return (*self_class).is_equivalent_to_impl(db, *other_class, inferable, visitor); + } + let self_normalized = self.normalized(db); if self_normalized == Type::ProtocolInstance(other) { return ConstraintSet::from(true); From 32a59c234d0fd75f07ced424aeaf55ca1b38d84f Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Mon, 23 Feb 2026 15:39:49 -0800 Subject: [PATCH 061/261] Include configured extensions in file discovery (#23400) --- crates/ruff/tests/cli/format.rs | 60 ++++++++++++++++++++++ crates/ruff_linter/src/settings/types.rs | 11 +++- crates/ruff_workspace/src/configuration.rs | 16 ++++-- crates/ruff_workspace/src/options.rs | 4 ++ ruff.schema.json | 2 +- 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/crates/ruff/tests/cli/format.rs b/crates/ruff/tests/cli/format.rs index 6e7c8a8117625..5e7dcbe6b2bf3 100644 --- a/crates/ruff/tests/cli/format.rs +++ b/crates/ruff/tests/cli/format.rs @@ -2512,3 +2512,63 @@ fn markdown_formatting_stdin() -> Result<()> { "#); Ok(()) } + +#[test] +fn format_mapped_extension_files() -> Result<()> { + let test = CliTest::with_files([ + ( + "pyproject.toml", + r#" +[tool.ruff] +extension = {foo="python", bar="markdown"} +"#, + ), + ( + "test.foo", + r" +print( 'hello' ) +", + ), + ( + "test.bar", + r" +Text string + +```py +print( 'hello' ) +``` +", + ), + ])?; + + assert_cmd_snapshot!( + test.format_command() + .args(["format", "--preview", "--check", "."]), + @r#" + success: false + exit_code: 2 + ----- stdout ----- + io: [TMP]/format: No such file or directory (os error 2) + --> format:1:1 + + unformatted: File would be reformatted + --> test.bar:1:1 + 2 | Text string + 3 | + 4 | ```py + - print( 'hello' ) + 5 + print("hello") + 6 | ``` + + unformatted: File would be reformatted + --> test.foo:1:1 + - + - print( 'hello' ) + 1 + print("hello") + + 2 files would be reformatted + + ----- stderr ----- + "#); + Ok(()) +} diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index feb23afc6c64d..c80d0d24415d9 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -200,6 +200,7 @@ impl Deref for GlobPath { #[derive(Debug, Clone, CacheKey, PartialEq, PartialOrd, Eq, Ord)] pub enum FilePattern { Builtin(&'static str), + Config(String), User(String, GlobPath), } @@ -209,6 +210,9 @@ impl FilePattern { FilePattern::Builtin(pattern) => { builder.add(Glob::from_str(pattern)?); } + FilePattern::Config(pattern) => { + builder.add(Glob::new(&pattern)?); + } FilePattern::User(pattern, absolute) => { // Add the absolute path. builder.add(Glob::new(&absolute.to_string_lossy())?); @@ -230,7 +234,7 @@ impl Display for FilePattern { "{:?}", match self { Self::Builtin(pattern) => pattern, - Self::User(pattern, _) => pattern.as_str(), + Self::User(pattern, _) | Self::Config(pattern) => pattern.as_str(), } ) } @@ -498,6 +502,11 @@ impl From for (String, Language) { pub struct ExtensionMapping(FxHashMap); impl ExtensionMapping { + /// Return the file extensions in the mapping. + pub fn extensions(&self) -> impl Iterator { + self.0.keys() + } + /// Return the [`Language`] for the given file. pub fn get(&self, path: &Path) -> Option { let ext = path.extension()?.to_str()?; diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index dbf31290d17c7..c2c6857d70a30 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -279,9 +279,19 @@ impl Configuration { PreviewMode::Disabled => FilePatternSet::try_from_iter( self.include.unwrap_or_else(|| INCLUDE.to_vec()), )?, - PreviewMode::Enabled => FilePatternSet::try_from_iter( - self.include.unwrap_or_else(|| INCLUDE_PREVIEW.to_vec()), - )?, + PreviewMode::Enabled => { + FilePatternSet::try_from_iter(self.include.unwrap_or_else(|| { + let mut patterns = INCLUDE_PREVIEW.to_vec(); + if let Some(extension_map) = &self.extension { + patterns.extend( + extension_map + .extensions() + .map(|ext| FilePattern::Config(format!("*.{ext}"))), + ); + } + patterns + }))? + } }, respect_gitignore: self.respect_gitignore.unwrap_or(true), project_root: project_root.to_path_buf(), diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 78e35073a1d04..f7b77e286e861 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -286,6 +286,10 @@ pub struct Options { /// by the `--extension` command-line flag). /// /// Supported file types include `python`, `pyi`, `ipynb`, and `markdown`. + /// + /// Any file extensions listed here will be automatically added to the + /// default `include` list as a `*.{ext}` glob, so that they are linted + /// and formatted without needing any additional configuration settings. #[option( default = "{}", value_type = "dict[str, Language]", diff --git a/ruff.schema.json b/ruff.schema.json index 0f1e7cf4d60f0..ebcd8c1f39c97 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -178,7 +178,7 @@ } }, "extension": { - "description": "A mapping of custom file extensions to known file types (overridden\nby the `--extension` command-line flag).\n\nSupported file types include `python`, `pyi`, `ipynb`, and `markdown`.", + "description": "A mapping of custom file extensions to known file types (overridden\nby the `--extension` command-line flag).\n\nSupported file types include `python`, `pyi`, `ipynb`, and `markdown`.\n\nAny file extensions listed here will be automatically added to the\ndefault `include` list as a `*.{ext}` glob, so that they are linted\nand formatted without needing any additional configuration settings.", "type": [ "object", "null" From 7e041f2e5d3ca5f354af71168467f41df31d8cac Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Mon, 23 Feb 2026 15:49:26 -0800 Subject: [PATCH 062/261] [ty] expanded/corrected comments for the Python <3.13 `Generator` special case (#23528) --- .../resources/mdtest/protocols.md | 34 ++++++++++++++++--- .../ty_python_semantic/src/types/instance.rs | 10 +++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index f0655ba1c12b2..ed58ea7161f74 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -3396,10 +3396,36 @@ def g(x: B2[int]): ## The `Generator` protocol's `_ReturnT_co` needs special casing prior to Python 3.13 -Prior to Python 3.13, the `_ReturnT_co` type parameter doesn't appear in any `Generator` methods in -typeshed (except `__iter__`, which returns the self type recursively and gets normalized to `Any`). -We need to special-case `Generator` so that specializations that differ in their return type don't -appear equivalent. +The `_ReturnT_co` type parameter in the `Generator` protocol is the value of a `yield from` over +that generator, and it's also in the pathway for the return values from `async` functions. (In the +`Awaitable` protocol, `__await__` returns a `Generator`.) So of course if we're asking whether one +type of `Generator` is e.g. assignable to another, and we see that one of them has a `_ReturnT_co` +type of `float` while the other has `str`, we should decide that they're not assignable. + +However, zooming in to the implementation details, `_ReturnT_co` is actually the type of the `value` +attribute on the `StopIteration` exception that the `Generator` raises when it's finished. This is +awkward, because protocols don't describe the exceptions that their methods raise. How is `ty` +supposed to see that incompatible `_ReturnT_co` types imply incompatible `Generator`s? + +As of Python 3.13, the `Generator` protocol's `close` method was changed from returning `None` to +returning `_ReturnT_co | None`. This was motivated by an edge case (you tried to cancel a generator, +but it caught the related exception and returned something anyway), but coincidentally it tells `ty` +what it needs to know: `_ReturnT_co` is something that some method in this protocol returns. +Something with a method that returns `float` isn't assignable to something where the same method +returns `str`. Problem solved. + +However, prior to 3.13, the `_ReturnT_co` type only appeared in the `__iter__` method. +Unfortunately, the `__iter__` method on a `Generator` just returns `self`; its return type is the +same `Generator`. That isn't helpful for the assignability question, because all we can say by +looking at `__iter__` is that "`Generator` `A` is assignable to `Generator` `B` if...`Generator` `A` +is assignable to `Generator` `B`." In practice we break this recursive cycle by inserting `Any`, and +we end up ignoring `_ReturnT_co` entirely and saying that things are assignable when they shouldn't +be. But how we break the cycle isn't really the problem; the problem is that the `Generator` +protocol (prior to 3.13) genuinely tells us nothing about how `_ReturnT_co` interacts with +assignability. + +As a special case workaround for this, we compare `Generator` implementations *nominally* when the +target Python version is 3.12 or earlier, in both `has_relation_to` and `is_equivalent_to`. ```toml [environment] diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 266827d2d9e37..c84378248cd6c 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -184,8 +184,8 @@ impl<'db> Type<'db> { } // `Generator` special case: Prior to 3.13, the `_ReturnT_co` type didn't appear in any - // methods (except `__iter__`, but that returns the self type recursively, which gets - // normalized to `Any`). We don't want generators with different return types to be + // methods (except `__iter__`, but that returns the self type recursively, so it can't rule + // out assignability). We don't want generators with different return types to be // assignable to each other. In this case we use the result of the nominal check above. if let Some(self_protocol) = self.as_protocol_instance() && let Protocol::FromClass(self_class) = self_protocol.inner @@ -826,9 +826,9 @@ impl<'db> ProtocolInstanceType<'db> { } // `Generator` special case: Prior to 3.13, the `_ReturnT_co` type didn't appear in any - // methods (except `__iter__`, but that returns the self type recursively, which gets - // normalized to `Any`). We don't want generators with different return types to be - // equivalent to each other. In this case we compare the `ClassType`s nominally. + // methods (except `__iter__`, but that returns the self type recursively, so it can't rule + // out equivalence). We don't want generators with different return types to be equivalent + // to each other. In this case we compare the `ClassType`s nominally. if let Protocol::FromClass(self_class) = self.inner && let Protocol::FromClass(other_class) = other.inner && self_class.known(db) == Some(KnownClass::Generator) From 906316810d9eda4101a904a08187646e3efefec4 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 23 Feb 2026 18:23:12 -0800 Subject: [PATCH 063/261] [ty] fix protocol generic inference with literal integers (#23534) ## Summary Fixes https://github.com/astral-sh/ty/issues/2483 Fix matching of literal integers to generic protocols. ## Test Plan Added mdtest. --- .../resources/mdtest/protocols.md | 25 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 6 ++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index ed58ea7161f74..8496b7bdb27f8 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2042,6 +2042,31 @@ static_assert(is_assignable_to(str, SupportsLessThan)) static_assert(is_assignable_to(int, Invertable)) ``` +Literal values should satisfy protocols with method members via their instance fallback type: + +```py +from typing import Literal, Protocol, TypeVar + +reveal_type(abs(5)) # revealed: int + +def f(x: Literal[5]) -> None: + reveal_type(abs(x)) # revealed: int + +InT = TypeVar("InT") +OutT = TypeVar("OutT") + +class CanMul(Protocol[InT, OutT]): + def __mul__(self, x: InT, /) -> OutT: ... + +def x2(x: CanMul[int, OutT], /) -> OutT: + return x * 2 + +def g(x: int) -> None: + reveal_type(x2(x)) # revealed: int + +reveal_type(x2(1)) # revealed: int +``` + ## Subtyping of protocols with generic method members Protocol method members can be generic. They can have generic contexts scoped to the class: diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 62c9b69c1d2dd..509a3d1bf5be9 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -2258,9 +2258,9 @@ impl<'db> SpecializationBuilder<'db> { ( formal @ (Type::NominalInstance(_) | Type::ProtocolInstance(_)), Type::LiteralValue(literal), - ) if literal.is_string() || literal.is_literal_string() || literal.is_bytes() => { - // Retry specialization with the literal's fallback instance (`str` / `bytes`) - // so literal iterables can contribute to generic inference. + ) => { + // Retry specialization with the literal's fallback instance so literals can + // contribute to generic inference for nominal and protocol formals. let actual_instance = literal.fallback_instance(self.db); return self.infer_map_impl(formal, actual_instance, polarity, f, seen); } From dec65ad3cdb44a3dbe768d933ab0538f7f14a12e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 24 Feb 2026 02:25:55 +0000 Subject: [PATCH 064/261] [ty] Fixup some tests added in #23472 (#23529) Addresses https://github.com/astral-sh/ruff/pull/23472/changes#r2842812274 --- .../resources/mdtest/type_of/generics.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index 757200e5350a8..6d30d11b56245 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -516,9 +516,9 @@ def f[ ]( bare_type: type, type_object: type[object], + type_int: type[int], type_t_unbound: type[T], type_t_object_bound: type[T1], - type_int: type[int], type_t_int_bound: type[T2], type_t_union_bound: type[T3], type_t_constrained: type[T4], @@ -534,13 +534,23 @@ def f[ reveal_type(type_object("")) # revealed: Any reveal_type(type_t_unbound()) # revealed: T@f + # TODO: we could consider emitting an error here as well reveal_type(type_t_unbound("")) # revealed: T@f + reveal_type(type_t_object_bound()) # revealed: T1@f + # TODO: we could consider emitting an error here as well + reveal_type(type_t_object_bound("")) # revealed: T1@f + reveal_type(type_int()) # revealed: int reveal_type(type_int("1")) # revealed: int # error: [invalid-argument-type] reveal_type(type_int([])) # revealed: int + reveal_type(type_t_int_bound()) # revealed: T2@f + reveal_type(type_t_int_bound("1")) # revealed: T2@f + # error: [invalid-argument-type] + reveal_type(type_t_int_bound([])) # revealed: T2@f + reveal_type(type_t_union_bound(42)) # revealed: T3@f # error: [invalid-argument-type] reveal_type(type_t_union_bound(42, "")) # revealed: T3@f From fe0a1d16b497e928b008240456d9693535358fd7 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:41:33 +0900 Subject: [PATCH 065/261] [ty] Isolate loop header reachability evaluation in tracked function (#23520) --- crates/ty_python_semantic/src/place.rs | 162 +++++++++++++----- .../src/semantic_index/use_def.rs | 9 +- .../src/types/infer/builder.rs | 35 +--- 3 files changed, 138 insertions(+), 68 deletions(-) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 11e0b1ceb7535..0885e9c899ffe 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -6,6 +6,7 @@ use ty_module_resolver::{ use crate::dunder_all::dunder_all_names; use crate::semantic_index::definition::{Definition, DefinitionKind, DefinitionState}; +use crate::semantic_index::narrowing_constraints::ScopedNarrowingConstraint; use crate::semantic_index::place::{PlaceExprRef, ScopedPlaceId}; use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{ @@ -18,7 +19,7 @@ use crate::types::{ Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, binding_type, declaration_type, }; -use crate::{Db, FxOrderSet, Program}; +use crate::{Db, FxIndexSet, FxOrderSet, Program}; pub(crate) use implicit_globals::{ module_type_implicit_global_declaration, module_type_implicit_global_symbol, @@ -1153,6 +1154,122 @@ fn place_impl<'db>( .unwrap_or_default() } +/// Pre-computed reachability analysis for loop-back bindings in a loop header. +#[salsa::tracked( + cycle_initial=|db, _, definition| loop_header_reachability_impl(db, definition, true), + cycle_fn=loop_header_reachability_cycle_recover, + heap_size = ruff_memory_usage::heap_size, +)] +pub(crate) fn loop_header_reachability<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> LoopHeaderReachability<'db> { + loop_header_reachability_impl(db, definition, false) +} + +fn loop_header_reachability_cycle_recover<'db>( + _db: &'db dyn Db, + _cycle: &salsa::Cycle, + previous: &LoopHeaderReachability<'db>, + result: LoopHeaderReachability<'db>, + _definition: Definition<'db>, +) -> LoopHeaderReachability<'db> { + result.cycle_normalized(previous) +} + +fn loop_header_reachability_impl<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + is_cycle_initial: bool, +) -> LoopHeaderReachability<'db> { + let DefinitionKind::LoopHeader(loop_header_definition) = definition.kind(db) else { + unreachable!("`loop_header_reachability` called with non-loop-header definition"); + }; + + let scope = definition.scope(db); + let use_def = use_def_map(db, scope); + let loop_header = get_loop_header(db, loop_header_definition.loop_token()); + let place = loop_header_definition.place(); + + let mut has_defined_bindings = false; + let mut deleted_reachability = Truthiness::AlwaysFalse; + let mut reachable_bindings = FxIndexSet::default(); + + for live_binding in loop_header.bindings_for_place(place) { + let reachability = if is_cycle_initial { + Truthiness::Ambiguous + } else { + use_def.evaluate_reachability(db, live_binding.reachability_constraint) + }; + // Skip unreachable bindings. + if reachability.is_always_false() { + continue; + } + + match use_def.definition(live_binding.binding) { + DefinitionState::Defined(def) => { + has_defined_bindings = true; + if def != definition { + reachable_bindings.insert(ReachableLoopBinding { + definition: def, + narrowing_constraint: live_binding.narrowing_constraint, + }); + } + } + // `del` in the loop body is always visible to code after the loop via the + // normal control flow merge. Updating `deleted_reachability` here is + // necessary for prior uses in the loop to see it. + DefinitionState::Deleted => { + deleted_reachability = deleted_reachability.or(reachability); + } + // If UNBOUND is visible at loop-back, then it was visible before the loop. + // Loop header definitions don't shadow preexisting bindings, so we don't + // need to do anything with this. + DefinitionState::Undefined => {} + } + } + + LoopHeaderReachability { + has_defined_bindings, + deleted_reachability, + reachable_bindings, + } +} + +/// Result of [`loop_header_reachability`]: pre-computed reachability info for loop-back bindings. +#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) struct LoopHeaderReachability<'db> { + /// Whether any reachable loop-back binding is a defined binding. + pub(crate) has_defined_bindings: bool, + pub(crate) deleted_reachability: Truthiness, + /// Reachable, defined loop-back bindings (excluding the loop header definition itself). + pub(crate) reachable_bindings: FxIndexSet>, +} + +impl<'db> LoopHeaderReachability<'db> { + fn cycle_normalized( + self, + previous: &LoopHeaderReachability<'db>, + ) -> LoopHeaderReachability<'db> { + let mut reachable_bindings = FxIndexSet::default(); + reachable_bindings.extend(previous.reachable_bindings.iter().copied()); + reachable_bindings.extend(self.reachable_bindings); + + LoopHeaderReachability { + has_defined_bindings: self.has_defined_bindings, + deleted_reachability: self.deleted_reachability, + reachable_bindings, + } + } +} + +/// A single reachable loop-back binding with its narrowing constraint. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) struct ReachableLoopBinding<'db> { + pub(crate) definition: Definition<'db>, + pub(crate) narrowing_constraint: ScopedNarrowingConstraint, +} + /// Implementation of [`place_from_bindings`]. /// /// ## Implementation Note @@ -1163,7 +1280,6 @@ fn place_from_bindings_impl<'db>( bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, ) -> PlaceWithDefinition<'db> { - let all_definitions = bindings_with_constraints.all_definitions; let predicates = bindings_with_constraints.predicates; let reachability_constraints = bindings_with_constraints.reachability_constraints; let boundness_analysis = bindings_with_constraints.boundness_analysis; @@ -1278,48 +1394,14 @@ fn place_from_bindings_impl<'db>( // We need to "look through" loop header definitions to do boundness analysis. The // actual type is computed by `infer_loop_header_definition` via `binding_type` below, // like all other bindings, so that it can participate in fixpoint iteration. - if let DefinitionKind::LoopHeader(loop_header_kind) = binding.kind(db) { - let loop_header = get_loop_header(db, loop_header_kind.loop_token()); - let place = loop_header_kind.place(); - let mut has_defined_bindings = false; - for loop_back in loop_header.bindings_for_place(place) { - // Skip unreachable bindings. - if reachability_constraints - .evaluate(db, predicates, loop_back.reachability_constraint) - .is_always_false() - { - continue; - } - - // Resolve the definition state from the binding ID. - let def_state = all_definitions[loop_back.binding]; - - match def_state { - DefinitionState::Defined(_) => { - has_defined_bindings = true; - } - // `del` in the loop body is always visible to code after the loop via the - // normal control flow merge. Updating `deleted_reachability` here is - // necessary for prior uses in the loop to see it. - DefinitionState::Deleted => { - deleted_reachability = - deleted_reachability.or(reachability_constraints.evaluate( - db, - predicates, - loop_back.reachability_constraint, - )); - } - // If UNBOUND is visible at loop-back, then it was visible before the loop. - // Loop header definitions don't shadow preexisting bindings, so we don't - // need to do anything with this. - DefinitionState::Undefined => {} - } - } + if binding.kind(db).is_loop_header() { + let loop_header = loop_header_reachability(db, binding); + deleted_reachability = deleted_reachability.or(loop_header.deleted_reachability); // If all the bindings in the loop are in statically false branches, it might be // that none of them loop-back. In that case short-circuit, so that we don't // produce an `Unknown` fallback type, and so that `Place::Undefined` is still a // possibility below. - if !has_defined_bindings { + if !loop_header.has_defined_bindings { return None; } } else { diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index c0597e87a8e51..3e8e6fed7ceab 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -397,9 +397,16 @@ impl<'db> UseDefMap<'db> { db: &dyn crate::Db, reachability: ScopedReachabilityConstraintId, ) -> bool { + self.evaluate_reachability(db, reachability).may_be_true() + } + + pub(crate) fn evaluate_reachability( + &self, + db: &dyn crate::Db, + reachability: ScopedReachabilityConstraintId, + ) -> crate::types::Truthiness { self.reachability_constraints .evaluate(db, &self.predicates, reachability) - .may_be_true() } pub(crate) fn definition(&self, id: ScopedDefinitionId) -> DefinitionState<'db> { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f05127b4dc1f4..a1a325e56ec79 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -37,9 +37,9 @@ use crate::node_key::NodeKey; use crate::place::{ ConsideredDefinitions, DefinedPlace, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin, builtins_module_scope, builtins_symbol, class_body_implicit_symbol, - explicit_global_symbol, global_symbol, module_type_implicit_global_declaration, - module_type_implicit_global_symbol, place, place_from_bindings, place_from_declarations, - typing_extensions_symbol, + explicit_global_symbol, global_symbol, loop_header_reachability, + module_type_implicit_global_declaration, module_type_implicit_global_symbol, place, + place_from_bindings, place_from_declarations, typing_extensions_symbol, }; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId}; @@ -57,7 +57,7 @@ use crate::semantic_index::scope::{ use crate::semantic_index::symbol::{ScopedSymbolId, Symbol}; use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, attribute_assignments, - get_loop_header, place_table, + place_table, }; use crate::types::builder::RecursivelyDefined; use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex}; @@ -5020,37 +5020,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, ) { let db = self.db(); - let loop_token = loop_header_kind.loop_token(); let place = loop_header_kind.place(); - let loop_header = get_loop_header(db, loop_token); let use_def = self .index .use_def_map(self.scope().file_scope_id(self.db())); + let loop_header = loop_header_reachability(db, definition); let mut union = UnionBuilder::new(db).recursively_defined(RecursivelyDefined::Yes); - for live_binding in loop_header.bindings_for_place(place) { - // Skip unreachable bindings. - if !use_def.is_reachable(db, live_binding.reachability_constraint) { - continue; - } - - // Boundness analysis is handled by looking at these bindings again in - // `place_from_bindings_impl`. Here we're only concerned with the type. - let def_state = use_def.definition(live_binding.binding); - let def = match def_state { - DefinitionState::Defined(def) => def, - DefinitionState::Deleted | DefinitionState::Undefined => continue, - }; - - // This loop header is visible to itself. Filter it out to avoid a pointless cycle. - if def == definition { - continue; - } - - let binding_ty = binding_type(db, def); + for reachable_binding in &loop_header.reachable_bindings { + let binding_ty = binding_type(db, reachable_binding.definition); let narrowed_ty = use_def - .narrowing_evaluator(live_binding.narrowing_constraint) + .narrowing_evaluator(reachable_binding.narrowing_constraint) .narrow(db, binding_ty, place); union.add_in_place(narrowed_ty); From 8dab7e4d09c5e732a1b6e3a33eb4c4e2bdb26a3c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 24 Feb 2026 10:00:06 +0000 Subject: [PATCH 066/261] [ty] Improve diagnostics for subscriptions of non-generic types (#23516) ## Summary This PR contains some minor followups to https://github.com/astral-sh/ruff/pull/21840 to improve our diagnostics in a few places. I've had this patch hanging around locally for a while and can't remember exactly what I had thought there still was to do on it anymore... I think these are still worthwhile improvements, anyway! ## Test Plan Snapshots --- .../mdtest/generics/pep695/aliases.md | 45 ++++++++--- .../resources/mdtest/implicit_type_aliases.md | 45 ++++++++--- ...rbose\342\200\246_(17ec595c7d02a324).snap" | 64 +++++++++++++++ ...erbos\342\200\246_(c495f90628efc0f0).snap" | 81 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 14 ++-- .../src/types/infer/builder.rs | 24 +++--- .../types/infer/builder/type_expression.rs | 24 ++++-- .../ty_python_semantic/src/types/subscript.rs | 16 ++-- 8 files changed, 260 insertions(+), 53 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/aliases.md_-_Generic_type_aliases\342\200\246_-_Snapshots_of_verbose\342\200\246_(17ec595c7d02a324).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/implicit_type_aliase\342\200\246_-_Implicit_type_aliase\342\200\246_-_Generic_implicit_typ\342\200\246_-_Snapshots_for_verbos\342\200\246_(c495f90628efc0f0).snap" diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index b22eb6caf923f..0df6c1da64d93 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -72,32 +72,32 @@ from typing import TypeVar, Protocol, TypedDict type B = ... -# error: [not-subscriptable] "Cannot subscript non-generic type alias" +# error: [not-subscriptable] "Cannot subscript non-generic type alias `B`" reveal_type(B[int]) # revealed: Unknown -# error: [not-subscriptable] "Cannot subscript non-generic type alias" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `B`" def _(b: B[int]): reveal_type(b) # revealed: Unknown type IntOrStr = int | str -# error: [not-subscriptable] "Cannot subscript non-generic type alias" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `IntOrStr`" def _(c: IntOrStr[int]): reveal_type(c) # revealed: Unknown type ListOfInts = list[int] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `ListOfInts`" def _(l: ListOfInts[int]): reveal_type(l) # revealed: Unknown type List[T] = list[T] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed" +# error: [not-subscriptable] "Cannot specialize non-generic type alias: Double specialization is not allowed" def _(l: List[int][int]): reveal_type(l) # revealed: Unknown -# error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type ``" type DoubleSpecialization[T] = list[T][T] def _(d: DoubleSpecialization[int]): @@ -105,7 +105,7 @@ def _(d: DoubleSpecialization[int]): type Tuple = tuple[int, str] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: `tuple[int, str]` is already specialized" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `Tuple`" def _(doubly_specialized: Tuple[int]): reveal_type(doubly_specialized) # revealed: Unknown @@ -116,7 +116,7 @@ class LegacyProto(Protocol[T]): type LegacyProtoInt = LegacyProto[int] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: `LegacyProto[int]` is already specialized" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `LegacyProtoInt`" def _(x: LegacyProtoInt[int]): reveal_type(x) # revealed: Unknown @@ -125,7 +125,7 @@ class Proto[T](Protocol): type ProtoInt = Proto[int] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: `Proto[int]` is already specialized" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `ProtoInt`" def _(x: ProtoInt[int]): reveal_type(x) # revealed: Unknown @@ -135,7 +135,7 @@ class LegacyDict(TypedDict[T]): type LegacyDictInt = LegacyDict[int] -# error: [not-subscriptable] "Cannot subscript non-generic type alias" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `LegacyDictInt`" def _(x: LegacyDictInt[int]): reveal_type(x) # revealed: Unknown @@ -144,13 +144,13 @@ class Dict[T](TypedDict): type DictInt = Dict[int] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: `Dict[int]` is already specialized" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `DictInt`" def _(x: DictInt[int]): reveal_type(x) # revealed: Unknown type Union = list[str] | list[int] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: `list[str] | list[int]` is already specialized" +# error: [not-subscriptable] "Cannot specialize non-generic type alias `Union`" def _(x: Union[int]): reveal_type(x) # revealed: Unknown ``` @@ -244,6 +244,27 @@ def _(g: G): reveal_type(g) # revealed: list[int] ``` +## Snapshots of verbose diagnostics + + + +```py +class A: ... +class B[T]: ... + +type AliasA = A +type AliasB = B[int] + +# fmt: off + +def f( + a: AliasA[int], # error: [not-subscriptable] + b: AliasB[int], # error: [not-subscriptable] +): ... + +# fmt: on +``` + ## Aliases are not callable ```py diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 292b15ec32ace..129fa3d3867a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -664,18 +664,18 @@ from typing import Protocol, TypeVar, TypedDict ListOfInts = list[int] -# error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type ``" def _(doubly_specialized: ListOfInts[int]): reveal_type(doubly_specialized) # revealed: Unknown type ListOfInts2 = list[int] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type alias `ListOfInts2`" DoublySpecialized = ListOfInts2[int] def _(doubly_specialized: DoublySpecialized): reveal_type(doubly_specialized) # revealed: Unknown -# error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type ``" List = list[int][int] def _(doubly_specialized: List): @@ -683,7 +683,7 @@ def _(doubly_specialized: List): Tuple = tuple[int, str] -# error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type ``" def _(doubly_specialized: Tuple[int]): reveal_type(doubly_specialized) # revealed: Unknown @@ -694,7 +694,7 @@ class LegacyProto(Protocol[T]): LegacyProtoInt = LegacyProto[int] -# error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type ``" def _(doubly_specialized: LegacyProtoInt[int]): reveal_type(doubly_specialized) # revealed: Unknown @@ -703,7 +703,7 @@ class Proto[T](Protocol): ProtoInt = Proto[int] -# error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type ``" def _(doubly_specialized: ProtoInt[int]): reveal_type(doubly_specialized) # revealed: Unknown @@ -724,20 +724,20 @@ class Dict[T](TypedDict): DictInt = Dict[int] -# error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type ``" def _(doubly_specialized: DictInt[int]): reveal_type(doubly_specialized) # revealed: Unknown Union = list[str] | list[int] -# error: [not-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +# error: [not-subscriptable] "Cannot subscript non-generic type ``" def _(doubly_specialized: Union[int]): reveal_type(doubly_specialized) # revealed: Unknown type MyListAlias[T] = list[T] MyListOfInts = MyListAlias[int] -# error: [not-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed" +# error: [not-subscriptable] "Cannot specialize non-generic type alias: Double specialization is not allowed" def _(doubly_specialized: MyListOfInts[int]): reveal_type(doubly_specialized) # revealed: Unknown ``` @@ -792,6 +792,33 @@ def _(): reveal_type(x) # revealed: Unknown ``` +### Snapshots for verbose diagnostics + + + +```toml +[environment] +python-version = "3.12" +``` + +```py +type ListOfInts2 = list[int] + +# error: [not-subscriptable] "Cannot subscript non-generic type alias `ListOfInts2`" +DoublySpecialized = ListOfInts2[int] + +ThreeInts = tuple[int, int, int] + +class A[T]: ... + +AliasForA = A[int] + +def f( + a: AliasForA[int], # error: [not-subscriptable] + b: ThreeInts[int], # error: [not-subscriptable] +): ... +``` + ### Multiple definitions #### Shadowed definitions diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/aliases.md_-_Generic_type_aliases\342\200\246_-_Snapshots_of_verbose\342\200\246_(17ec595c7d02a324).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/aliases.md_-_Generic_type_aliases\342\200\246_-_Snapshots_of_verbose\342\200\246_(17ec595c7d02a324).snap" new file mode 100644 index 0000000000000..cf524ef435fcb --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/aliases.md_-_Generic_type_aliases\342\200\246_-_Snapshots_of_verbose\342\200\246_(17ec595c7d02a324).snap" @@ -0,0 +1,64 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: aliases.md - Generic type aliases: PEP 695 syntax - Snapshots of verbose diagnostics +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class A: ... + 2 | class B[T]: ... + 3 | + 4 | type AliasA = A + 5 | type AliasB = B[int] + 6 | + 7 | # fmt: off + 8 | + 9 | def f( +10 | a: AliasA[int], # error: [not-subscriptable] +11 | b: AliasB[int], # error: [not-subscriptable] +12 | ): ... +13 | +14 | # fmt: on +``` + +# Diagnostics + +``` +error[not-subscriptable]: Cannot specialize non-generic type alias `AliasA` + --> src/mdtest_snippet.py:10:8 + | + 9 | def f( +10 | a: AliasA[int], # error: [not-subscriptable] + | ------^^^^^ + | | + | Alias to `A`, which is not generic +11 | b: AliasB[int], # error: [not-subscriptable] +12 | ): ... + | +info: rule `not-subscriptable` is enabled by default + +``` + +``` +error[not-subscriptable]: Cannot specialize non-generic type alias `AliasB` + --> src/mdtest_snippet.py:11:8 + | + 9 | def f( +10 | a: AliasA[int], # error: [not-subscriptable] +11 | b: AliasB[int], # error: [not-subscriptable] + | ------^^^^^ + | | + | Alias to `B[int]`, which is already specialized +12 | ): ... + | +info: rule `not-subscriptable` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/implicit_type_aliase\342\200\246_-_Implicit_type_aliase\342\200\246_-_Generic_implicit_typ\342\200\246_-_Snapshots_for_verbos\342\200\246_(c495f90628efc0f0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/implicit_type_aliase\342\200\246_-_Implicit_type_aliase\342\200\246_-_Generic_implicit_typ\342\200\246_-_Snapshots_for_verbos\342\200\246_(c495f90628efc0f0).snap" new file mode 100644 index 0000000000000..04406ffcd0130 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/implicit_type_aliase\342\200\246_-_Implicit_type_aliase\342\200\246_-_Generic_implicit_typ\342\200\246_-_Snapshots_for_verbos\342\200\246_(c495f90628efc0f0).snap" @@ -0,0 +1,81 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: implicit_type_aliases.md - Implicit type aliases - Generic implicit type aliases - Snapshots for verbose diagnostics +mdtest path: crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | type ListOfInts2 = list[int] + 2 | + 3 | # error: [not-subscriptable] "Cannot subscript non-generic type alias `ListOfInts2`" + 4 | DoublySpecialized = ListOfInts2[int] + 5 | + 6 | ThreeInts = tuple[int, int, int] + 7 | + 8 | class A[T]: ... + 9 | +10 | AliasForA = A[int] +11 | +12 | def f( +13 | a: AliasForA[int], # error: [not-subscriptable] +14 | b: ThreeInts[int], # error: [not-subscriptable] +15 | ): ... +``` + +# Diagnostics + +``` +error[not-subscriptable]: Cannot subscript non-generic type alias `ListOfInts2` + --> src/mdtest_snippet.py:4:21 + | +3 | # error: [not-subscriptable] "Cannot subscript non-generic type alias `ListOfInts2`" +4 | DoublySpecialized = ListOfInts2[int] + | -----------^^^^^ + | | + | Alias to `list[int]`, which is already specialized +5 | +6 | ThreeInts = tuple[int, int, int] + | +info: rule `not-subscriptable` is enabled by default + +``` + +``` +error[not-subscriptable]: Cannot subscript non-generic type `` + --> src/mdtest_snippet.py:13:8 + | +12 | def f( +13 | a: AliasForA[int], # error: [not-subscriptable] + | ---------^^^^^ + | | + | Type is already specialized +14 | b: ThreeInts[int], # error: [not-subscriptable] +15 | ): ... + | +info: rule `not-subscriptable` is enabled by default + +``` + +``` +error[not-subscriptable]: Cannot subscript non-generic type `` + --> src/mdtest_snippet.py:14:8 + | +12 | def f( +13 | a: AliasForA[int], # error: [not-subscriptable] +14 | b: ThreeInts[int], # error: [not-subscriptable] + | ---------^^^^^ + | | + | Type is already specialized +15 | ): ... + | +info: rule `not-subscriptable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d7d82eed577d1..da750ad32af28 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1022,23 +1022,25 @@ impl<'db> Type<'db> { matches!(self, Type::GenericAlias(_)) } - /// Returns whether the definition of this type is generic - /// (this is different from whether this type *is* a generic type; a type that is already fully specialized is not a generic type). - pub(crate) fn is_definition_generic(self, db: &'db dyn Db) -> bool { + /// Returns whether this type represents a specialization of a generic type. + /// + /// For example, whereas `` is a generic type, `` + /// is a specialization of that type. + pub(crate) fn is_specialized_generic(self, db: &'db dyn Db) -> bool { match self { Type::Union(union) => union .elements(db) .iter() - .any(|ty| ty.is_definition_generic(db)), + .any(|ty| ty.is_specialized_generic(db)), Type::Intersection(intersection) => { intersection .positive(db) .iter() - .any(|ty| ty.is_definition_generic(db)) + .any(|ty| ty.is_specialized_generic(db)) || intersection .negative(db) .iter() - .any(|ty| ty.is_definition_generic(db)) + .any(|ty| ty.is_specialized_generic(db)) } Type::NominalInstance(instance_type) => instance_type.is_definition_generic(), Type::ProtocolInstance(protocol) => { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index a1a325e56ec79..917011941db15 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4891,8 +4891,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Ok(param_type); } - Type::KnownInstance(known_instance @ KnownInstanceType::TypeVar(typevar)) - if known_instance.class(self.db()) == KnownClass::ParamSpec => + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) + if typevar.is_paramspec(db) => { if let Some(diagnostic_builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, expr) @@ -16473,12 +16473,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // that's generic over a legacy TypeVarTuple } else if typevars_len == 0 { // Type parameter list cannot be empty, so if we reach here, `value_ty` is not a generic type. - if let Some(builder) = self - .context - .report_lint(&NOT_SUBSCRIPTABLE, &*subscript.value) - { - let mut diagnostic = - builder.into_diagnostic("Cannot subscript non-generic type"); + if let Some(builder) = self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type `{}`", + value_ty.display(db) + )); if match value_ty { Type::GenericAlias(_) => true, Type::KnownInstance(KnownInstanceType::UnionType(union)) => union @@ -16486,10 +16485,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .is_ok_and(|mut tys| tys.any(|ty| ty.is_generic_alias())), _ => false, } { - diagnostic.set_primary_message(format_args!( - "`{}` is already specialized", - value_ty.display(db) - )); + diagnostic.annotate( + self.context + .secondary(&*subscript.value) + .message("Type is already specialized"), + ); } } error = Some(ExplicitSpecializationError::NonGeneric); diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index f41fce6f6fc9e..b413f072276aa 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1023,7 +1023,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { let mut diagnostic = - builder.into_diagnostic("Cannot subscript non-generic type alias"); + builder.into_diagnostic("Cannot specialize non-generic type alias"); diagnostic.set_primary_message("Double specialization is not allowed"); } return Type::unknown(); @@ -1052,14 +1052,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot specialize non-generic type alias `{}`", + type_alias.name(self.db()) + )); + let secondary = self.context.secondary(&*subscript.value); let value_type = type_alias.raw_value_type(self.db()); - let mut diagnostic = builder - .into_diagnostic("Cannot subscript non-generic type alias"); - if value_type.is_definition_generic(self.db()) { - diagnostic.set_primary_message(format_args!( - "`{}` is already specialized", - value_type.display(self.db()), - )); + if value_type.is_specialized_generic(self.db()) { + diagnostic.annotate(secondary.message(format_args!( + "Alias to `{}`, which is already specialized", + value_type.display(self.db()) + ))); + } else { + diagnostic.annotate(secondary.message(format_args!( + "Alias to `{}`, which is not generic", + value_type.display(self.db()) + ))); } } diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index 3404c312e2a64..994fecc1ba461 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -207,13 +207,17 @@ impl<'db> SubscriptErrorKind<'db> { } Self::NonGenericTypeAlias { alias } => { if let Some(builder) = context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type alias `{}`", + alias.name(db) + )); let value_type = alias.raw_value_type(db); - let mut diagnostic = - builder.into_diagnostic("Cannot subscript non-generic type alias"); - if value_type.is_definition_generic(db) { - diagnostic.set_primary_message(format_args!( - "`{}` is already specialized", - value_type.display(db) + if value_type.is_specialized_generic(db) { + diagnostic.annotate(context.secondary(&*subscript.value).message( + format_args!( + "Alias to `{}`, which is already specialized", + value_type.display(db) + ), )); } } From 5a86c879130748d8beade73b4a9278b619baa313 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 24 Feb 2026 11:55:39 +0000 Subject: [PATCH 067/261] [ty] Refactor SpecialFormType to use nested enums for type qualifiers and aliases (#23517) --- .../resources/mdtest/implicit_type_aliases.md | 8 +- .../resources/mdtest/pep613_type_aliases.md | 7 +- crates/ty_python_semantic/src/types.rs | 145 +--- .../src/types/class_base.rs | 46 +- .../src/types/diagnostic.rs | 3 +- .../src/types/infer/builder.rs | 404 +++++----- .../infer/builder/annotation_expression.rs | 291 ++++--- .../types/infer/builder/type_expression.rs | 90 +-- crates/ty_python_semantic/src/types/narrow.rs | 27 +- .../src/types/special_form.rs | 731 ++++++++++++------ 10 files changed, 867 insertions(+), 885 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 129fa3d3867a0..3d687c73bd74a 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -1470,7 +1470,7 @@ from typing import List, Dict # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" InvalidList = List[1] -# error: [invalid-type-form] "`typing.List` requires exactly one argument" +# error: [invalid-type-form] "`typing.List` requires exactly 1 argument, got 2" ListTooManyArgs = List[int, str] # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" @@ -1479,10 +1479,10 @@ InvalidDict1 = Dict[1, str] # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" InvalidDict2 = Dict[str, 2] -# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 1" +# error: [invalid-type-form] "`typing.Dict` requires exactly 2 arguments, got 1" DictTooFewArgs = Dict[str] -# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 3" +# error: [invalid-type-form] "`typing.Dict` requires exactly 2 arguments, got 3" DictTooManyArgs = Dict[str, int, float] def _( @@ -1497,7 +1497,7 @@ def _( reveal_type(list_too_many_args) # revealed: list[Unknown] reveal_type(invalid_dict1) # revealed: dict[Unknown, str] reveal_type(invalid_dict2) # revealed: dict[str, Unknown] - reveal_type(dict_too_few_args) # revealed: dict[str, Unknown] + reveal_type(dict_too_few_args) # revealed: dict[Unknown, Unknown] reveal_type(dict_too_many_args) # revealed: dict[Unknown, Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index ad901d4042c28..8b1bb49e30d2d 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -478,9 +478,14 @@ bad4: TypeAlias = Final # error: [invalid-type-form] bad5: TypeAlias = Required[int] # error: [invalid-type-form] bad6: TypeAlias = NotRequired[int] # error: [invalid-type-form] bad7: TypeAlias = ReadOnly[int] # error: [invalid-type-form] -bad8: TypeAlias = Unpack[tuple[int, ...]] # error: [invalid-type-form] bad9: TypeAlias = InitVar[int] # error: [invalid-type-form] bad10: TypeAlias = InitVar # error: [invalid-type-form] + +# TODO: this should cause us to emit an error (`Unpack` is not valid at the +# top level in this context), but for different reasons to the above cases: +# `Unpack` is not a type qualifier, and so the error message in our diagnostic +# shouldn't say that it is. +differently_bad: TypeAlias = Unpack[tuple[int, ...]] ``` [type expression]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index da750ad32af28..c8bb86c34897b 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1,5 +1,4 @@ use compact_str::ToCompactString; -use infer::nearest_enclosing_class; use itertools::{Either, Itertools}; use ruff_diagnostics::{Edit, Fix}; use rustc_hash::FxHashMap; @@ -63,8 +62,7 @@ use crate::types::function::{ FunctionType, KnownFunction, }; use crate::types::generics::{ - ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, typing_self, - walk_generic_context, + ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, walk_generic_context, }; pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; use crate::types::mro::{Mro, MroIterator, StaticMroError}; @@ -75,6 +73,7 @@ pub(crate) use crate::types::narrow::{ use crate::types::newtype::NewType; pub(crate) use crate::types::signatures::{Parameter, Parameters}; use crate::types::signatures::{ParameterForm, walk_signature}; +use crate::types::special_form::TypeQualifier; use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder}; use crate::types::typed_dict::TypedDictField; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; @@ -6027,134 +6026,9 @@ impl<'db> Type<'db> { KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), }, - Type::SpecialForm(special_form) => match special_form { - SpecialFormType::Never | SpecialFormType::NoReturn => Ok(Type::Never), - SpecialFormType::LiteralString => Ok(Type::literal_string()), - SpecialFormType::Any => Ok(Type::any()), - SpecialFormType::Unknown => Ok(Type::unknown()), - SpecialFormType::AlwaysTruthy => Ok(Type::AlwaysTruthy), - SpecialFormType::AlwaysFalsy => Ok(Type::AlwaysFalsy), - - // We treat `typing.Type` exactly the same as `builtins.type`: - SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), - SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), - - // Legacy `typing` aliases - SpecialFormType::List => Ok(KnownClass::List.to_instance(db)), - SpecialFormType::Dict => Ok(KnownClass::Dict.to_instance(db)), - SpecialFormType::Set => Ok(KnownClass::Set.to_instance(db)), - SpecialFormType::FrozenSet => Ok(KnownClass::FrozenSet.to_instance(db)), - SpecialFormType::ChainMap => Ok(KnownClass::ChainMap.to_instance(db)), - SpecialFormType::Counter => Ok(KnownClass::Counter.to_instance(db)), - SpecialFormType::DefaultDict => Ok(KnownClass::DefaultDict.to_instance(db)), - SpecialFormType::Deque => Ok(KnownClass::Deque.to_instance(db)), - SpecialFormType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)), - - // TODO: Use an opt-in rule for a bare `Callable` - SpecialFormType::Callable => Ok(Type::Callable(CallableType::unknown(db))), - - // Special case: `NamedTuple` in a type expression is understood to describe the type - // `tuple[object, ...] & `. - // This isn't very principled (since at runtime, `NamedTuple` is just a function), - // but it appears to be what users often expect, and it improves compatibility with - // other type checkers such as mypy. - // See conversation in https://github.com/astral-sh/ruff/pull/19915. - SpecialFormType::NamedTuple => Ok(IntersectionType::from_elements( - db, - [ - Type::homogeneous_tuple(db, Type::object()), - KnownClass::NamedTupleLike.to_instance(db), - ], - )), - SpecialFormType::TypingSelf => { - let index = semantic_index(db, scope_id.file(db)); - let Some(class) = nearest_enclosing_class(db, index, scope_id) else { - return Err(InvalidTypeExpressionError { - fallback_type: Type::unknown(), - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::InvalidType(*self, scope_id) - ], - }); - }; - - Ok( - typing_self(db, scope_id, typevar_binding_context, class.into()) - .map(Type::TypeVar) - .unwrap_or(*self), - ) - } - // We ensure that `typing.TypeAlias` used in the expected position (annotating an - // annotated assignment statement) doesn't reach here. Using it in any other type - // expression is an error. - SpecialFormType::TypeAlias => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![InvalidTypeExpression::TypeAlias], - fallback_type: Type::unknown(), - }), - SpecialFormType::TypedDict => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![InvalidTypeExpression::TypedDict], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Literal - | SpecialFormType::Union - | SpecialFormType::Intersection => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::RequiresArguments(*special_form) - ], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Protocol => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![InvalidTypeExpression::Protocol], - fallback_type: Type::unknown(), - }), - SpecialFormType::Generic => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![InvalidTypeExpression::Generic], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Optional - | SpecialFormType::Not - | SpecialFormType::Top - | SpecialFormType::Bottom - | SpecialFormType::TypeOf - | SpecialFormType::TypeIs - | SpecialFormType::TypeGuard - | SpecialFormType::Unpack - | SpecialFormType::CallableTypeOf => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::RequiresOneArgument(*special_form) - ], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Annotated | SpecialFormType::Concatenate => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::RequiresTwoArguments(*special_form) - ], - fallback_type: Type::unknown(), - }) - } - - SpecialFormType::ClassVar | SpecialFormType::Final => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::TypeQualifier(*special_form) - ], - fallback_type: Type::unknown(), - }) - } - - SpecialFormType::ReadOnly - | SpecialFormType::NotRequired - | SpecialFormType::Required => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::TypeQualifierRequiresOneArgument(*special_form) - ], - fallback_type: Type::unknown(), - }), - }, + Type::SpecialForm(special_form) => { + special_form.in_type_expression(db, scope_id, typevar_binding_context) + } Type::Union(union) => { let mut builder = UnionBuilder::new(db); @@ -7992,9 +7866,10 @@ impl<'db> TypeAndQualifiers<'db> { self.origin } - /// Insert/add an additional type qualifier. - pub(crate) fn add_qualifier(&mut self, qualifier: TypeQualifiers) { + /// Return `self` with an additional qualifier added to the set of qualifiers. + pub(crate) fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { self.qualifiers |= qualifier; + self } /// Return the set of type qualifiers. @@ -8079,10 +7954,10 @@ enum InvalidTypeExpression<'db> { TypeAlias, /// Type qualifiers are always invalid in *type expressions*, /// but these ones are okay with 0 arguments in *annotation expressions* - TypeQualifier(SpecialFormType), + TypeQualifier(TypeQualifier), /// Type qualifiers that are invalid in type expressions, /// and which would require exactly one argument even if they appeared in an annotation expression - TypeQualifierRequiresOneArgument(SpecialFormType), + TypeQualifierRequiresOneArgument(TypeQualifier), /// Some types are always invalid in type expressions InvalidType(Type<'db>, ScopeId<'db>), } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 5e12ec91c9e4f..a8f1ea4722fe0 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,14 +1,13 @@ use crate::types::class::CodeGeneratorKind; use crate::types::generics::{ApplySpecialization, Specialization}; use crate::types::mro::MroIterator; -use crate::{Db, DisplaySettings}; - use crate::types::tuple::TupleType; use crate::types::{ ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, SpecialFormType, StaticMroError, Type, TypeContext, TypeMapping, todo_type, }; +use crate::{Db, DisplaySettings}; /// Enumeration of the possible kinds of types we allow in class bases. /// @@ -213,23 +212,20 @@ impl<'db> ClassBase<'db> { }, Type::SpecialForm(special_form) => match special_form { + SpecialFormType::TypeQualifier(_) => None, + SpecialFormType::Annotated | SpecialFormType::Literal | SpecialFormType::LiteralString | SpecialFormType::Union | SpecialFormType::NoReturn | SpecialFormType::Never - | SpecialFormType::Final - | SpecialFormType::NotRequired | SpecialFormType::TypeGuard | SpecialFormType::TypeIs | SpecialFormType::TypingSelf | SpecialFormType::Unpack - | SpecialFormType::ClassVar | SpecialFormType::Concatenate - | SpecialFormType::Required | SpecialFormType::TypeAlias - | SpecialFormType::ReadOnly | SpecialFormType::Optional | SpecialFormType::Not | SpecialFormType::Top @@ -242,9 +238,9 @@ impl<'db> ClassBase<'db> { SpecialFormType::Any => Some(Self::Dynamic(DynamicType::Any)), SpecialFormType::Unknown => Some(Self::unknown()), - SpecialFormType::Protocol => Some(Self::Protocol), SpecialFormType::Generic => Some(Self::Generic), + SpecialFormType::TypedDict => Some(Self::TypedDict), SpecialFormType::NamedTuple => { let class = subclass?.as_static()?; @@ -261,41 +257,19 @@ impl<'db> ClassBase<'db> { ) } - // TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO - SpecialFormType::Dict => { - Self::try_from_type(db, KnownClass::Dict.to_class_literal(db), subclass) - } - SpecialFormType::List => { - Self::try_from_type(db, KnownClass::List.to_class_literal(db), subclass) - } + // TODO: Classes inheriting from `typing.Type` also have `Generic` in their MRO SpecialFormType::Type => { Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) } + SpecialFormType::Tuple => { Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass) } - SpecialFormType::Set => { - Self::try_from_type(db, KnownClass::Set.to_class_literal(db), subclass) - } - SpecialFormType::FrozenSet => { - Self::try_from_type(db, KnownClass::FrozenSet.to_class_literal(db), subclass) - } - SpecialFormType::ChainMap => { - Self::try_from_type(db, KnownClass::ChainMap.to_class_literal(db), subclass) - } - SpecialFormType::Counter => { - Self::try_from_type(db, KnownClass::Counter.to_class_literal(db), subclass) - } - SpecialFormType::DefaultDict => { - Self::try_from_type(db, KnownClass::DefaultDict.to_class_literal(db), subclass) - } - SpecialFormType::Deque => { - Self::try_from_type(db, KnownClass::Deque.to_class_literal(db), subclass) - } - SpecialFormType::OrderedDict => { - Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db), subclass) + + SpecialFormType::LegacyStdlibAlias(alias) => { + Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass) } - SpecialFormType::TypedDict => Some(Self::TypedDict), + SpecialFormType::Callable => Self::try_from_type( db, todo_type!("Support for Callable as a base class"), diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 6a828626ef47e..f708fd62e7ff9 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -3926,7 +3926,7 @@ pub(crate) fn report_invalid_arguments_to_annotated( pub(crate) fn report_invalid_argument_number_to_special_form( context: &InferContext, subscript: &ast::ExprSubscript, - special_form: SpecialFormType, + special_form: impl Into, received_arguments: usize, expected_arguments: u8, ) { @@ -3939,6 +3939,7 @@ pub(crate) fn report_invalid_argument_number_to_special_form( builder.into_diagnostic(format_args!( "Special form `{special_form}` expected exactly {expected_arguments} {noun}, \ got {received_arguments}", + special_form = special_form.into(), )); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 917011941db15..8411d86c198c5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -119,6 +119,7 @@ use crate::types::generics::{ use crate::types::infer::nearest_enclosing_function; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; +use crate::types::special_form::AliasSpec; use crate::types::subclass_of::SubclassOfInner; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleSpecBuilder, TupleType}; @@ -9141,11 +9142,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if is_pep_613_type_alias { let is_valid_special_form = |ty: Type<'db>| match ty { - Type::SpecialForm(special_form) => special_form.is_valid_in_type_expression(), - Type::ClassLiteral(literal) - if literal.is_known(self.db(), KnownClass::InitVar) => - { - false + Type::SpecialForm(SpecialFormType::TypeQualifier(_)) => false, + Type::ClassLiteral(literal) => { + !literal.is_known(self.db(), KnownClass::InitVar) } _ => true, }; @@ -15914,11 +15913,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } - Type::SpecialForm(SpecialFormType::Tuple) => { - return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(subscript)); - } - Type::SpecialForm(SpecialFormType::Literal) => { - match self.infer_literal_parameter_type(slice) { + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Tuple => { + return tuple_generic_alias( + self.db(), + self.infer_tuple_type_expression(subscript), + ); + } + SpecialFormType::Literal => match self.infer_literal_parameter_type(slice) { Ok(result) => { return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( self.db(), @@ -15938,255 +15940,213 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } return Type::unknown(); } - } - } - Type::SpecialForm(SpecialFormType::Annotated) => { - let ast::Expr::Tuple(ast::ExprTuple { - elts: ref arguments, - .. - }) = **slice - else { - report_invalid_arguments_to_annotated(&self.context, subscript); - - return self.infer_expression(slice, TypeContext::default()); - }; + }, + SpecialFormType::Annotated => { + let ast::Expr::Tuple(ast::ExprTuple { + elts: ref arguments, + .. + }) = **slice + else { + report_invalid_arguments_to_annotated(&self.context, subscript); - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } + return self.infer_expression(slice, TypeContext::default()); + }; - let [type_expr, metadata @ ..] = &arguments[..] else { - for argument in arguments { - self.infer_expression(argument, TypeContext::default()); + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); } - self.store_expression_type(slice, Type::unknown()); - return Type::unknown(); - }; - for element in metadata { - self.infer_expression(element, TypeContext::default()); - } + let [type_expr, metadata @ ..] = &arguments[..] else { + for argument in arguments { + self.infer_expression(argument, TypeContext::default()); + } + self.store_expression_type(slice, Type::unknown()); + return Type::unknown(); + }; - let ty = self.infer_type_expression(type_expr); + for element in metadata { + self.infer_expression(element, TypeContext::default()); + } - return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( - self.db(), - ty, - ))); - } - Type::SpecialForm(SpecialFormType::Optional) => { - let db = self.db(); + let ty = self.infer_type_expression(type_expr); - if matches!(**slice, ast::Expr::Tuple(_)) - && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "`typing.Optional` requires exactly one argument" - )); + return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( + self.db(), + ty, + ))); } + SpecialFormType::Optional => { + let db = self.db(); - let ty = self.infer_type_expression(slice); + if matches!(**slice, ast::Expr::Tuple(_)) + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "`typing.Optional` requires exactly one argument" + )); + } - // `Optional[None]` is equivalent to `None`: - if ty.is_none(db) { - return ty; + let ty = self.infer_type_expression(slice); + + // `Optional[None]` is equivalent to `None`: + if ty.is_none(db) { + return ty; + } + + return Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + None, + Ok(UnionType::from_elements(db, [ty, Type::none(db)])), + ), + )); } + SpecialFormType::Union => { + let db = self.db(); - return Type::KnownInstance(KnownInstanceType::UnionType(UnionTypeInstance::new( - db, - None, - Ok(UnionType::from_elements(db, [ty, Type::none(db)])), - ))); - } - Type::SpecialForm(SpecialFormType::Union) => { - let db = self.db(); + match **slice { + ast::Expr::Tuple(ref tuple) => { + let mut elements = tuple + .elts + .iter() + .map(|elt| self.infer_type_expression(elt)) + .peekable(); - match **slice { - ast::Expr::Tuple(ref tuple) => { - let mut elements = tuple - .elts - .iter() - .map(|elt| self.infer_type_expression(elt)) - .peekable(); + let is_empty = elements.peek().is_none(); + let union_type = Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + None, + Ok(UnionType::from_elements(db, elements)), + ), + )); - let is_empty = elements.peek().is_none(); - let union_type = Type::KnownInstance(KnownInstanceType::UnionType( - UnionTypeInstance::new( - db, - None, - Ok(UnionType::from_elements(db, elements)), - ), - )); + if is_empty + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic( + "`typing.Union` requires at least one type argument", + ); + } - if is_empty - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic( - "`typing.Union` requires at least one type argument", - ); + return union_type; + } + _ => { + return self.infer_expression(slice, TypeContext::default()); } - - return union_type; - } - _ => { - return self.infer_expression(slice, TypeContext::default()); } } - } - Type::SpecialForm(SpecialFormType::Type) => { - // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` - let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( - InternedType::new(self.db(), argument_ty), - )); - } - Type::SpecialForm(SpecialFormType::Callable) => { - let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { - &*tuple.elts - } else { - std::slice::from_ref(&*subscript.slice) - }; + SpecialFormType::Type => { + // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` + let argument_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + InternedType::new(self.db(), argument_ty), + )); + } + SpecialFormType::Callable => { + let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { + &*tuple.elts + } else { + std::slice::from_ref(&*subscript.slice) + }; - // TODO: Remove this once we support Concatenate properly. This is necessary - // to avoid a lot of false positives downstream, because we can't represent the typevar- - // specialized `Callable` types yet. - let num_arguments = arguments.len(); - if num_arguments == 2 { - let first_arg = &arguments[0]; - let second_arg = &arguments[1]; - - if first_arg.is_subscript_expr() { - let first_arg_ty = self.infer_expression(first_arg, TypeContext::default()); - if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) = - first_arg_ty - { - let mut variables = generic_context - .variables(self.db()) - .collect::>(); + // TODO: Remove this once we support Concatenate properly. This is necessary + // to avoid a lot of false positives downstream, because we can't represent the typevar- + // specialized `Callable` types yet. + let num_arguments = arguments.len(); + if num_arguments == 2 { + let first_arg = &arguments[0]; + let second_arg = &arguments[1]; + + if first_arg.is_subscript_expr() { + let first_arg_ty = + self.infer_expression(first_arg, TypeContext::default()); + if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) = + first_arg_ty + { + let mut variables = generic_context + .variables(self.db()) + .collect::>(); - let return_ty = - self.infer_expression(second_arg, TypeContext::default()); - return_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); + let return_ty = + self.infer_expression(second_arg, TypeContext::default()); + return_ty.bind_and_find_all_legacy_typevars( + self.db(), + self.typevar_binding_context, + &mut variables, + ); - let generic_context = - GenericContext::from_typevar_instances(self.db(), variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } + let generic_context = + GenericContext::from_typevar_instances(self.db(), variables); + return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); + } - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( "The first argument to `Callable` must be either a list of types, \ ParamSpec, Concatenate, or `...`", )); + } + return Type::KnownInstance(KnownInstanceType::Callable( + CallableType::unknown(self.db()), + )); } - return Type::KnownInstance(KnownInstanceType::Callable( - CallableType::unknown(self.db()), - )); } - } - let callable = self - .infer_callable_type(subscript) - .as_callable() - .expect("always returns Type::Callable"); + let callable = self + .infer_callable_type(subscript) + .as_callable() + .expect("always returns Type::Callable"); - return Type::KnownInstance(KnownInstanceType::Callable(callable)); - } - // `typing` special forms with a single generic argument - Type::SpecialForm( - special_form @ (SpecialFormType::List - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::Counter - | SpecialFormType::Deque), - ) => { - let slice_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::Callable(callable)); + } + SpecialFormType::LegacyStdlibAlias(alias) => { + let AliasSpec { + class, + expected_argument_number, + } = alias.alias_spec(); - let element_ty = if matches!(**slice, ast::Expr::Tuple(_)) { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly one argument", - special_form.name() - )); - } - Type::unknown() - } else { - slice_ty - }; + let args = if let ast::Expr::Tuple(t) = &**slice { + &*t.elts + } else { + std::slice::from_ref(&**slice) + }; - let class = special_form - .aliased_stdlib_class() - .expect("A known stdlib class is available"); - - return class - .to_specialized_class_type(self.db(), &[element_ty]) - .map(Type::from) - .unwrap_or_else(Type::unknown); - } - // `typing` special forms with two generic arguments - Type::SpecialForm( - special_form @ (SpecialFormType::Dict - | SpecialFormType::ChainMap - | SpecialFormType::DefaultDict - | SpecialFormType::OrderedDict), - ) => { - let (first_ty, second_ty) = if let ast::Expr::Tuple(ast::ExprTuple { - elts: ref arguments, - .. - }) = **slice - { - if arguments.len() != 2 - && let Some(builder) = + if args.len() != expected_argument_number { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly two arguments, got {}", - special_form.name(), - arguments.len() - )); - } - - if let [first_expr, second_expr] = &arguments[..] { - let first_ty = self.infer_type_expression(first_expr); - let second_ty = self.infer_type_expression(second_expr); - - (first_ty, second_ty) - } else { - for argument in arguments { - self.infer_type_expression(argument); + { + let noun = if expected_argument_number == 1 { + "argument" + } else { + "arguments" + }; + builder.into_diagnostic(format_args!( + "`typing.{name}` requires exactly \ + {expected_argument_number} {noun}, got {got}", + name = special_form.name(), + got = args.len() + )); } - - (Type::unknown(), Type::unknown()) } - } else { - let first_ty = self.infer_type_expression(slice); - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly two arguments, got 1", - special_form.name() - )); - } - - (first_ty, Type::unknown()) - }; + let arg_types: Vec<_> = args + .iter() + .map(|arg| self.infer_type_expression(arg)) + .collect(); - let class = special_form - .aliased_stdlib_class() - .expect("Stdlib class available"); + return class + .to_specialized_class_type(self.db(), arg_types) + .map(Type::from) + .unwrap_or_else(Type::unknown); + } + _ => {} + }, - return class - .to_specialized_class_type(self.db(), &[first_ty, second_ty]) - .map(Type::from) - .unwrap_or_else(Type::unknown); - } Type::KnownInstance( KnownInstanceType::UnionType(_) | KnownInstanceType::Annotated(_) diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index e57e43f056409..7a245bd545f33 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -10,7 +10,8 @@ use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, }; use crate::types::{ - KnownClass, SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifiers, todo_type, + KnownClass, SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifier, + TypeQualifiers, todo_type, }; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -89,37 +90,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder: &TypeInferenceBuilder<'db, '_>, pep_613_policy: PEP613Policy, ) -> TypeAndQualifiers<'db> { - match ty { - Type::SpecialForm(SpecialFormType::ClassVar) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::CLASS_VAR, - ), - Type::SpecialForm(SpecialFormType::Final) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::FINAL, - ), - Type::SpecialForm(SpecialFormType::Required) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::REQUIRED, - ), - Type::SpecialForm(SpecialFormType::NotRequired) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::NOT_REQUIRED, - ), - Type::SpecialForm(SpecialFormType::ReadOnly) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::READ_ONLY, - ), - Type::SpecialForm(SpecialFormType::TypeAlias) - if pep_613_policy == PEP613Policy::Allowed => - { - TypeAndQualifiers::declared(ty) - } + let special_case = match ty { + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::TypeQualifier(qualifier) => Some(TypeAndQualifiers::new( + Type::unknown(), + TypeOrigin::Declared, + TypeQualifiers::from(qualifier), + )), + SpecialFormType::TypeAlias if pep_613_policy == PEP613Policy::Allowed => { + Some(TypeAndQualifiers::declared(ty)) + } + _ => None, + }, // Conditional import of `typing.TypeAlias` or `typing_extensions.TypeAlias` on a // Python version where the former doesn't exist. Type::Union(union) @@ -131,7 +113,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) }) => { - TypeAndQualifiers::declared(Type::SpecialForm(SpecialFormType::TypeAlias)) + Some(TypeAndQualifiers::declared(Type::SpecialForm( + SpecialFormType::TypeAlias, + ))) } Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => { if let Some(builder) = @@ -140,13 +124,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder .into_diagnostic("`InitVar` may not be used without a type argument"); } - TypeAndQualifiers::new( + Some(TypeAndQualifiers::new( Type::unknown(), TypeOrigin::Declared, TypeQualifiers::INIT_VAR, - ) + )) } - _ => TypeAndQualifiers::declared( + _ => None, + }; + + special_case.unwrap_or_else(|| { + TypeAndQualifiers::declared( ty.default_specialize(builder.db()) .in_type_expression( builder.db(), @@ -160,8 +148,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.is_reachable(annotation), ) }), - ), - } + ) + }) } // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression @@ -228,135 +216,129 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let slice = &**slice; match value_ty { - Type::SpecialForm(SpecialFormType::Annotated) => { - // This branch is similar to the corresponding branch in `infer_parameterized_special_form_type_expression`, but - // `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly - // differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`). - if let ast::Expr::Tuple(ast::ExprTuple { - elts: arguments, .. - }) = slice - { - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } - - if let [inner_annotation, metadata @ ..] = &arguments[..] { - for element in metadata { - self.infer_expression(element, TypeContext::default()); + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Annotated => { + // This branch is similar to the corresponding branch in + // `infer_parameterized_special_form_type_expression`, but + // `Annotated[…]` can appear both in annotation expressions and in + // type expressions, and needs to be handled slightly + // differently in each case (calling either `infer_type_expression_*` + // or `infer_annotation_expression_*`). + if let ast::Expr::Tuple(ast::ExprTuple { + elts: arguments, .. + }) = slice + { + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); } - let inner_annotation_ty = self.infer_annotation_expression_impl( - inner_annotation, - PEP613Policy::Disallowed, - ); + if let [inner_annotation, metadata @ ..] = &arguments[..] { + for element in metadata { + self.infer_expression(element, TypeContext::default()); + } - self.store_expression_type(slice, inner_annotation_ty.inner_type()); - inner_annotation_ty - } else { - for argument in arguments { - self.infer_expression(argument, TypeContext::default()); + let inner_annotation_ty = self + .infer_annotation_expression_impl( + inner_annotation, + PEP613Policy::Disallowed, + ); + + self.store_expression_type( + slice, + inner_annotation_ty.inner_type(), + ); + inner_annotation_ty + } else { + for argument in arguments { + self.infer_expression(argument, TypeContext::default()); + } + self.store_expression_type(slice, Type::unknown()); + TypeAndQualifiers::declared(Type::unknown()) } - self.store_expression_type(slice, Type::unknown()); - TypeAndQualifiers::declared(Type::unknown()) + } else { + report_invalid_arguments_to_annotated(&self.context, subscript); + self.infer_annotation_expression_impl( + slice, + PEP613Policy::Disallowed, + ) } - } else { - report_invalid_arguments_to_annotated(&self.context, subscript); - self.infer_annotation_expression_impl(slice, PEP613Policy::Disallowed) } - } - Type::SpecialForm( - type_qualifier @ (SpecialFormType::ClassVar - | SpecialFormType::Final - | SpecialFormType::Required - | SpecialFormType::NotRequired - | SpecialFormType::ReadOnly), - ) => { - let arguments = if let ast::Expr::Tuple(tuple) = slice { - &*tuple.elts - } else { - std::slice::from_ref(slice) - }; - let type_and_qualifiers = if let [argument] = arguments { - let mut type_and_qualifiers = self.infer_annotation_expression_impl( - argument, - PEP613Policy::Disallowed, - ); - - // Emit a diagnostic if ClassVar and Final are combined in a class that is - // not a dataclass, since Final already implies the semantics of ClassVar. - let classvar_and_final = match type_qualifier { - SpecialFormType::Final => type_and_qualifiers - .qualifiers - .contains(TypeQualifiers::CLASS_VAR), - SpecialFormType::ClassVar => type_and_qualifiers - .qualifiers - .contains(TypeQualifiers::FINAL), - _ => false, + SpecialFormType::TypeQualifier(qualifier) => { + let arguments = if let ast::Expr::Tuple(tuple) = slice { + &*tuple.elts + } else { + std::slice::from_ref(slice) }; - if classvar_and_final - && nearest_enclosing_class(self.db(), self.index, self.scope()) - .is_none_or(|class| !class.is_dataclass_like(self.db())) - && let Some(builder) = self - .context - .report_lint(&REDUNDANT_FINAL_CLASSVAR, subscript) - { - builder.into_diagnostic(format_args!( - "`Combining `ClassVar` and `Final` is redundant" - )); - } + let type_and_qualifiers = if let [argument] = arguments { + let type_and_qualifiers = self.infer_annotation_expression_impl( + argument, + PEP613Policy::Disallowed, + ); + + // Emit a diagnostic if ClassVar and Final are combined in a class that is + // not a dataclass, since Final already implies the semantics of ClassVar. + let classvar_and_final = match qualifier { + TypeQualifier::Final => type_and_qualifiers + .qualifiers + .contains(TypeQualifiers::CLASS_VAR), + TypeQualifier::ClassVar => type_and_qualifiers + .qualifiers + .contains(TypeQualifiers::FINAL), + _ => false, + }; + if classvar_and_final + && nearest_enclosing_class(self.db(), self.index, self.scope()) + .is_none_or(|class| !class.is_dataclass_like(self.db())) + && let Some(builder) = self + .context + .report_lint(&REDUNDANT_FINAL_CLASSVAR, subscript) + { + builder.into_diagnostic(format_args!( + "`Combining `ClassVar` and `Final` is redundant" + )); + } - match type_qualifier { - SpecialFormType::ClassVar => { - type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR); - if type_and_qualifiers + if qualifier == TypeQualifier::ClassVar + && type_and_qualifiers .inner_type() .has_non_self_typevar(self.db()) - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic( - "`ClassVar` cannot contain type variables", - ); - } - } - SpecialFormType::Final => { - type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL); + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic( + "`ClassVar` cannot contain type variables", + ); } - SpecialFormType::Required => { - type_and_qualifiers.add_qualifier(TypeQualifiers::REQUIRED); - } - SpecialFormType::NotRequired => { - type_and_qualifiers.add_qualifier(TypeQualifiers::NOT_REQUIRED); + type_and_qualifiers.with_qualifier(TypeQualifiers::from(qualifier)) + } else { + for element in arguments { + self.infer_annotation_expression_impl( + element, + PEP613Policy::Disallowed, + ); } - SpecialFormType::ReadOnly => { - type_and_qualifiers.add_qualifier(TypeQualifiers::READ_ONLY); + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + let num_arguments = arguments.len(); + builder.into_diagnostic(format_args!( + "Type qualifier `{qualifier}` expected exactly 1 \ + argument, got {num_arguments}", + )); } - _ => unreachable!(), + TypeAndQualifiers::declared(Type::unknown()) + }; + if slice.is_tuple_expr() { + self.store_expression_type(slice, type_and_qualifiers.inner_type()); } type_and_qualifiers - } else { - for element in arguments { - self.infer_annotation_expression_impl( - element, - PEP613Policy::Disallowed, - ); - } - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - let num_arguments = arguments.len(); - builder.into_diagnostic(format_args!( - "Type qualifier `{type_qualifier}` expected exactly 1 argument, \ - got {num_arguments}", - )); - } - TypeAndQualifiers::declared(Type::unknown()) - }; - if slice.is_tuple_expr() { - self.store_expression_type(slice, type_and_qualifiers.inner_type()); } - type_and_qualifiers - } + _ => TypeAndQualifiers::declared( + self.infer_subscript_type_expression_no_store( + subscript, slice, value_ty, + ), + ), + }, Type::ClassLiteral(class) if class.is_known(self.db(), KnownClass::InitVar) => { let arguments = if let ast::Expr::Tuple(tuple) = slice { &*tuple.elts @@ -364,12 +346,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { std::slice::from_ref(slice) }; let type_and_qualifiers = if let [argument] = arguments { - let mut type_and_qualifiers = self.infer_annotation_expression_impl( + self.infer_annotation_expression_impl( argument, PEP613Policy::Disallowed, - ); - type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR); - type_and_qualifiers + ) + .with_qualifier(TypeQualifiers::INIT_VAR) } else { for element in arguments { self.infer_annotation_expression_impl( diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index b413f072276aa..d68ec157ec724 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -11,6 +11,7 @@ use crate::types::diagnostic::{ use crate::types::generics::bind_typevar; use crate::types::infer::builder::InnerExpressionInferenceState; use crate::types::signatures::Signature; +use crate::types::special_form::{AliasSpec, LegacyStdlibAlias}; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::{ @@ -1221,9 +1222,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fn infer_parameterized_legacy_typing_alias( &mut self, subscript_node: &ast::ExprSubscript, - expected_arg_count: usize, - alias: SpecialFormType, - class: KnownClass, + alias: LegacyStdlibAlias, ) -> Type<'db> { let arguments = &*subscript_node.slice; let args = if let ast::Expr::Tuple(t) = arguments { @@ -1231,15 +1230,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { std::slice::from_ref(arguments) }; - if args.len() != expected_arg_count { + + let AliasSpec { + class, + expected_argument_number, + } = alias.alias_spec(); + + if args.len() != expected_argument_number { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript_node) { - let noun = if expected_arg_count == 1 { + let noun = if expected_argument_number == 1 { "argument" } else { "arguments" }; builder.into_diagnostic(format_args!( - "Legacy alias `{alias}` expected exactly {expected_arg_count} {noun}, \ + "Legacy alias `{alias}` expected exactly {expected_argument_number} {noun}, \ got {}", args.len() )); @@ -1309,7 +1314,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { callable_type } - pub(crate) fn infer_parameterized_special_form_type_expression( + fn infer_parameterized_special_form_type_expression( &mut self, subscript: &ast::ExprSubscript, special_form: SpecialFormType, @@ -1540,70 +1545,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } callable_type } - - SpecialFormType::ChainMap => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::ChainMap, - KnownClass::ChainMap, - ), - SpecialFormType::OrderedDict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::OrderedDict, - KnownClass::OrderedDict, - ), - SpecialFormType::Dict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::Dict, - KnownClass::Dict, - ), - SpecialFormType::List => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::List, - KnownClass::List, - ), - SpecialFormType::DefaultDict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::DefaultDict, - KnownClass::DefaultDict, - ), - SpecialFormType::Counter => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Counter, - KnownClass::Counter, - ), - SpecialFormType::Set => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Set, - KnownClass::Set, - ), - SpecialFormType::FrozenSet => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::FrozenSet, - KnownClass::FrozenSet, - ), - SpecialFormType::Deque => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Deque, - KnownClass::Deque, - ), - - SpecialFormType::ClassVar - | SpecialFormType::Final - | SpecialFormType::Required - | SpecialFormType::NotRequired - | SpecialFormType::ReadOnly => { + SpecialFormType::LegacyStdlibAlias(alias) => { + self.infer_parameterized_legacy_typing_alias(subscript, alias) + } + SpecialFormType::TypeQualifier(qualifier) => { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let diag = builder.into_diagnostic(format_args!( - "Type qualifier `{special_form}` is not allowed in type expressions \ + "Type qualifier `{qualifier}` is not allowed in type expressions \ (only in annotation expressions)", )); diagnostic::add_type_expression_reference_link(diag); @@ -1640,9 +1588,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - let diag = builder.into_diagnostic(format_args!( + let diag = builder.into_diagnostic( "Special form `typing.TypeGuard` expected exactly one type parameter", - )); + ); diagnostic::add_type_expression_reference_link(diag); } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 31f8b7fdffdfd..dc98dbf579534 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -228,17 +228,24 @@ impl ClassInfoConstraintFunction { ) } - // We don't have a good meta-type for `Callable`s right now, - // so only apply `isinstance()` narrowing, not `issubclass()` - Type::SpecialForm(SpecialFormType::Callable) - if self == ClassInfoConstraintFunction::IsInstance => - { - Some(Type::Callable(CallableType::unknown(db)).top_materialization(db)) - } + Type::SpecialForm(form) => match form { + SpecialFormType::LegacyStdlibAlias(alias) => { + self.generate_constraint(db, alias.aliased_class().to_class_literal(db)) + } + SpecialFormType::Tuple => { + self.generate_constraint(db, KnownClass::Tuple.to_class_literal(db)) + } + SpecialFormType::Type => { + self.generate_constraint(db, KnownClass::Type.to_class_literal(db)) + } + + // We don't have a good meta-type for `Callable`s right now, + // so only apply `isinstance()` narrowing, not `issubclass()` + SpecialFormType::Callable => (self == ClassInfoConstraintFunction::IsInstance) + .then(|| Type::Callable(CallableType::unknown(db)).top_materialization(db)), - Type::SpecialForm(special_form) => special_form - .aliased_stdlib_class() - .and_then(|class| self.generate_constraint(db, class.to_class_literal(db))), + _ => None, + }, Type::AlwaysFalsy | Type::AlwaysTruthy diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 7d2d43f97dcdf..b5c89466069e4 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -4,32 +4,55 @@ use super::{ClassType, Type, class::KnownClass}; use crate::db::Db; use crate::semantic_index::place::ScopedPlaceId; -use crate::semantic_index::{FileScopeId, place_table, use_def_map}; -use crate::types::TypeDefinition; +use crate::semantic_index::{ + FileScopeId, definition::Definition, place_table, scope::ScopeId, semantic_index, use_def_map, +}; +use crate::types::{ + CallableType, IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, + TypeDefinition, TypeQualifiers, generics::typing_self, infer::nearest_enclosing_class, +}; use ruff_db::files::File; -use std::str::FromStr; +use strum_macros::EnumString; use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; /// Enumeration of specific runtime symbols that are special enough /// that they can each be considered to inhabit a unique type. /// +/// The enum uses a nested structure: variants that fall into well-defined subcategories +/// (legacy stdlib aliases and type qualifiers) are represented as nested enums, +/// while other special forms that each require unique handling remain as direct variants. +/// /// # Ordering /// /// Ordering is stable and should be the same between runs. -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Hash, - salsa::Update, - PartialOrd, - Ord, - strum_macros::EnumString, - get_size2::GetSize, -)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] pub enum SpecialFormType { + /// Special forms that are simple aliases to classes elsewhere in the standard library. + LegacyStdlibAlias(LegacyStdlibAlias), + + /// Special forms that are type qualifiers + TypeQualifier(TypeQualifier), + + /// The special form `typing.Tuple`. + /// + /// While this is technically an alias to `builtins.tuple`, it requires special handling + /// for type-expression parsing. + Tuple, + + /// The special form `typing.Type`. + /// + /// While this is technically an alias to `builtins.type`, it requires special handling + /// for type-expression parsing. + Type, + + /// The special form `Callable`. + /// + /// While `typing.Callable` aliases `collections.abc.Callable`, we view both objects + /// as inhabiting the same special form type internally. Moreover, `Callable` requires + /// special handling for both type-expression parsing and `isinstance`/`issubclass` + /// narrowing. + Callable, + Any, /// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`) Annotated, @@ -45,28 +68,6 @@ pub enum SpecialFormType { NoReturn, /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) Never, - /// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`) - Tuple, - /// The symbol `typing.List` (which can also be found as `typing_extensions.List`) - List, - /// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`) - Dict, - /// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`) - Set, - /// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`) - FrozenSet, - /// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`) - ChainMap, - /// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`) - Counter, - /// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`) - DefaultDict, - /// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`) - Deque, - /// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`) - OrderedDict, - /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) - Type, /// The symbol `ty_extensions.Unknown` Unknown, /// The symbol `ty_extensions.AlwaysTruthy` @@ -85,24 +86,12 @@ pub enum SpecialFormType { Top, /// The symbol `ty_extensions.Bottom` Bottom, - /// The symbol `typing.Callable` - /// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`) - Callable, /// The symbol `typing.Self` (which can also be found as `typing_extensions.Self`) - #[strum(serialize = "Self")] TypingSelf, - /// The symbol `typing.Final` (which can also be found as `typing_extensions.Final`) - Final, - /// The symbol `typing.ClassVar` (which can also be found as `typing_extensions.ClassVar`) - ClassVar, /// The symbol `typing.Concatenate` (which can also be found as `typing_extensions.Concatenate`) Concatenate, /// The symbol `typing.Unpack` (which can also be found as `typing_extensions.Unpack`) Unpack, - /// The symbol `typing.Required` (which can also be found as `typing_extensions.Required`) - Required, - /// The symbol `typing.NotRequired` (which can also be found as `typing_extensions.NotRequired`) - NotRequired, /// The symbol `typing.TypeAlias` (which can also be found as `typing_extensions.TypeAlias`) TypeAlias, /// The symbol `typing.TypeGuard` (which can also be found as `typing_extensions.TypeGuard`) @@ -111,8 +100,6 @@ pub enum SpecialFormType { TypedDict, /// The symbol `typing.TypeIs` (which can also be found as `typing_extensions.TypeIs`) TypeIs, - /// The symbol `typing.ReadOnly` (which can also be found as `typing_extensions.ReadOnly`) - ReadOnly, /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) /// @@ -146,13 +133,9 @@ impl SpecialFormType { | Self::Tuple | Self::Type | Self::TypingSelf - | Self::Final - | Self::ClassVar | Self::Callable | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypedDict @@ -163,7 +146,7 @@ impl SpecialFormType { | Self::Bottom | Self::Intersection | Self::CallableTypeOf - | Self::ReadOnly => KnownClass::SpecialForm, + | Self::TypeQualifier(_) => KnownClass::SpecialForm, // Typeshed says it's an instance of `_SpecialForm`, // but then we wouldn't recognise things like `issubclass(`X, Protocol)` @@ -172,15 +155,7 @@ impl SpecialFormType { Self::Generic | Self::Any => KnownClass::Type, - Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::Deque - | Self::ChainMap - | Self::OrderedDict => KnownClass::StdlibAlias, + Self::LegacyStdlibAlias(_) => KnownClass::StdlibAlias, Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy => KnownClass::Object, @@ -207,28 +182,196 @@ impl SpecialFormType { file: File, symbol_name: &str, ) -> Option { - let candidate = Self::from_str(symbol_name).ok()?; + let candidate = Self::from_name(symbol_name)?; candidate .check_module(file_to_module(db, file)?.known(db)?) .then_some(candidate) } + /// Parse a `SpecialFormType` from its runtime symbol name. + fn from_name(name: &str) -> Option { + /// An enum that maps 1:1 with `SpecialFormType`, but which holds no associated data + /// (and therefore can have `EnumString` derived on it). + /// This is much more robust than having a manual `from_string` method that matches + /// on string literals, because experience has shown it's very easy to forget to + /// update such a method when adding new variants. + #[derive(EnumString)] + enum SpecialFormTypeBuilder { + Tuple, + Type, + Callable, + Any, + Annotated, + Literal, + LiteralString, + Optional, + Union, + NoReturn, + Never, + Unknown, + AlwaysTruthy, + AlwaysFalsy, + Not, + Intersection, + TypeOf, + CallableTypeOf, + Top, + Bottom, + #[strum(serialize = "Self")] + TypingSelf, + Concatenate, + Unpack, + TypeAlias, + TypeGuard, + TypedDict, + TypeIs, + Protocol, + Generic, + NamedTuple, + List, + Dict, + FrozenSet, + Set, + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, + Final, + ClassVar, + ReadOnly, + Required, + NotRequired, + } + + // This implementation exists purely to enforce that every variant of `SpecialFormType` + // is included in the `SpecialFormTypeBuilder` enum + #[cfg(test)] + impl From for SpecialFormTypeBuilder { + fn from(value: SpecialFormType) -> Self { + match value { + SpecialFormType::AlwaysFalsy => Self::AlwaysFalsy, + SpecialFormType::AlwaysTruthy => Self::AlwaysTruthy, + SpecialFormType::Annotated => Self::Annotated, + SpecialFormType::Callable => Self::Callable, + SpecialFormType::CallableTypeOf => Self::CallableTypeOf, + SpecialFormType::Concatenate => Self::Concatenate, + SpecialFormType::Intersection => Self::Intersection, + SpecialFormType::Literal => Self::Literal, + SpecialFormType::LiteralString => Self::LiteralString, + SpecialFormType::Never => Self::Never, + SpecialFormType::NoReturn => Self::NoReturn, + SpecialFormType::Not => Self::Not, + SpecialFormType::Optional => Self::Optional, + SpecialFormType::Protocol => Self::Protocol, + SpecialFormType::Type => Self::Type, + SpecialFormType::TypeAlias => Self::TypeAlias, + SpecialFormType::TypeGuard => Self::TypeGuard, + SpecialFormType::TypeIs => Self::TypeIs, + SpecialFormType::TypingSelf => Self::TypingSelf, + SpecialFormType::Union => Self::Union, + SpecialFormType::Unknown => Self::Unknown, + SpecialFormType::Generic => Self::Generic, + SpecialFormType::NamedTuple => Self::NamedTuple, + SpecialFormType::Any => Self::Any, + SpecialFormType::Bottom => Self::Bottom, + SpecialFormType::Top => Self::Top, + SpecialFormType::Unpack => Self::Unpack, + SpecialFormType::Tuple => Self::Tuple, + SpecialFormType::TypedDict => Self::TypedDict, + SpecialFormType::TypeOf => Self::TypeOf, + SpecialFormType::LegacyStdlibAlias(alias) => match alias { + LegacyStdlibAlias::List => Self::List, + LegacyStdlibAlias::Dict => Self::Dict, + LegacyStdlibAlias::Set => Self::Set, + LegacyStdlibAlias::FrozenSet => Self::FrozenSet, + LegacyStdlibAlias::ChainMap => Self::ChainMap, + LegacyStdlibAlias::Counter => Self::Counter, + LegacyStdlibAlias::DefaultDict => Self::DefaultDict, + LegacyStdlibAlias::Deque => Self::Deque, + LegacyStdlibAlias::OrderedDict => Self::OrderedDict, + }, + SpecialFormType::TypeQualifier(qualifier) => match qualifier { + TypeQualifier::Final => Self::Final, + TypeQualifier::ClassVar => Self::ClassVar, + TypeQualifier::ReadOnly => Self::ReadOnly, + TypeQualifier::Required => Self::Required, + TypeQualifier::NotRequired => Self::NotRequired, + }, + } + } + } + + SpecialFormTypeBuilder::try_from(name) + .ok() + .map(|form| match form { + SpecialFormTypeBuilder::AlwaysFalsy => Self::AlwaysFalsy, + SpecialFormTypeBuilder::AlwaysTruthy => Self::AlwaysTruthy, + SpecialFormTypeBuilder::Annotated => Self::Annotated, + SpecialFormTypeBuilder::Callable => Self::Callable, + SpecialFormTypeBuilder::CallableTypeOf => Self::CallableTypeOf, + SpecialFormTypeBuilder::Concatenate => Self::Concatenate, + SpecialFormTypeBuilder::Intersection => Self::Intersection, + SpecialFormTypeBuilder::Literal => Self::Literal, + SpecialFormTypeBuilder::LiteralString => Self::LiteralString, + SpecialFormTypeBuilder::Never => Self::Never, + SpecialFormTypeBuilder::NoReturn => Self::NoReturn, + SpecialFormTypeBuilder::Not => Self::Not, + SpecialFormTypeBuilder::Optional => Self::Optional, + SpecialFormTypeBuilder::Protocol => Self::Protocol, + SpecialFormTypeBuilder::Type => Self::Type, + SpecialFormTypeBuilder::TypeAlias => Self::TypeAlias, + SpecialFormTypeBuilder::TypeGuard => Self::TypeGuard, + SpecialFormTypeBuilder::TypeIs => Self::TypeIs, + SpecialFormTypeBuilder::TypingSelf => Self::TypingSelf, + SpecialFormTypeBuilder::Union => Self::Union, + SpecialFormTypeBuilder::Unknown => Self::Unknown, + SpecialFormTypeBuilder::Generic => Self::Generic, + SpecialFormTypeBuilder::NamedTuple => Self::NamedTuple, + SpecialFormTypeBuilder::Any => Self::Any, + SpecialFormTypeBuilder::Bottom => Self::Bottom, + SpecialFormTypeBuilder::Top => Self::Top, + SpecialFormTypeBuilder::Unpack => Self::Unpack, + SpecialFormTypeBuilder::Tuple => Self::Tuple, + SpecialFormTypeBuilder::TypedDict => Self::TypedDict, + SpecialFormTypeBuilder::TypeOf => Self::TypeOf, + SpecialFormTypeBuilder::List => Self::LegacyStdlibAlias(LegacyStdlibAlias::List), + SpecialFormTypeBuilder::Dict => Self::LegacyStdlibAlias(LegacyStdlibAlias::Dict), + SpecialFormTypeBuilder::Set => Self::LegacyStdlibAlias(LegacyStdlibAlias::Set), + SpecialFormTypeBuilder::FrozenSet => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet) + } + SpecialFormTypeBuilder::ChainMap => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap) + } + SpecialFormTypeBuilder::Counter => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::Counter) + } + SpecialFormTypeBuilder::DefaultDict => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) + } + SpecialFormTypeBuilder::Deque => Self::LegacyStdlibAlias(LegacyStdlibAlias::Deque), + SpecialFormTypeBuilder::OrderedDict => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) + } + SpecialFormTypeBuilder::Final => Self::TypeQualifier(TypeQualifier::Final), + SpecialFormTypeBuilder::ClassVar => Self::TypeQualifier(TypeQualifier::ClassVar), + SpecialFormTypeBuilder::ReadOnly => Self::TypeQualifier(TypeQualifier::ReadOnly), + SpecialFormTypeBuilder::Required => Self::TypeQualifier(TypeQualifier::Required), + SpecialFormTypeBuilder::NotRequired => { + Self::TypeQualifier(TypeQualifier::NotRequired) + } + }) + } + /// Return `true` if `module` is a module from which this `SpecialFormType` variant can validly originate. /// /// Most variants can only exist in one module, which is the same as `self.class().canonical_module(db)`. /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. pub(super) fn check_module(self, module: KnownModule) -> bool { match self { - Self::ClassVar - | Self::Deque - | Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::ChainMap - | Self::OrderedDict + Self::TypeQualifier(TypeQualifier::ClassVar) + | Self::LegacyStdlibAlias(_) | Self::Optional | Self::Union | Self::NoReturn @@ -241,11 +384,14 @@ impl SpecialFormType { | Self::Literal | Self::LiteralString | Self::Never - | Self::Final + | Self::TypeQualifier( + TypeQualifier::Final + | TypeQualifier::Required + | TypeQualifier::NotRequired + | TypeQualifier::ReadOnly, + ) | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypedDict @@ -253,8 +399,7 @@ impl SpecialFormType { | Self::TypingSelf | Self::Protocol | Self::NamedTuple - | Self::Any - | Self::ReadOnly => { + | Self::Any => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } @@ -281,16 +426,30 @@ impl SpecialFormType { match self { // TypedDict can be called as a constructor to create TypedDict types Self::TypedDict + // Collection constructors are callable // TODO actually implement support for calling them - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::NamedTuple - | Self::OrderedDict => true, + | Self::LegacyStdlibAlias( + LegacyStdlibAlias::ChainMap + | LegacyStdlibAlias::Counter + | LegacyStdlibAlias::DefaultDict + | LegacyStdlibAlias::Deque + | LegacyStdlibAlias::OrderedDict + ) + | Self::NamedTuple => true, + + // Unlike the aliases to `collections` classes, + // the aliases to builtin classes are *not* callable... + Self::LegacyStdlibAlias( + LegacyStdlibAlias::List + | LegacyStdlibAlias::Dict + | LegacyStdlibAlias::Set + | LegacyStdlibAlias::FrozenSet + ) + | Self::Tuple + | Self::Type => false, - // All other special forms are not callable + // All other special forms are also not callable Self::Annotated | Self::Literal | Self::LiteralString @@ -298,12 +457,6 @@ impl SpecialFormType { | Self::Union | Self::NoReturn | Self::Never - | Self::Tuple - | Self::List - | Self::Dict - | Self::Set - | Self::FrozenSet - | Self::Type | Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy @@ -315,144 +468,24 @@ impl SpecialFormType { | Self::CallableTypeOf | Self::Callable | Self::TypingSelf - | Self::Final - | Self::ClassVar + | Self::TypeQualifier(_) | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypeIs - | Self::ReadOnly | Self::Protocol | Self::Any | Self::Generic => false, } } - /// Return `true` if this special form type is valid in a type-expression context (and not - /// just in an *annotation* expression context). See the following section of the typing - /// specification for more details: - /// - pub(super) const fn is_valid_in_type_expression(self) -> bool { - match self { - Self::ClassVar - | Self::Final - | Self::Required - | Self::NotRequired - | SpecialFormType::ReadOnly - | SpecialFormType::Unpack - | SpecialFormType::TypeAlias => false, - Self::Annotated - | SpecialFormType::Any - | SpecialFormType::Literal - | SpecialFormType::LiteralString - | SpecialFormType::Optional - | SpecialFormType::Union - | SpecialFormType::NoReturn - | SpecialFormType::Never - | SpecialFormType::Tuple - | SpecialFormType::List - | SpecialFormType::Dict - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::ChainMap - | SpecialFormType::Counter - | SpecialFormType::DefaultDict - | SpecialFormType::Deque - | SpecialFormType::OrderedDict - | SpecialFormType::Type - | SpecialFormType::Unknown - | SpecialFormType::AlwaysTruthy - | SpecialFormType::AlwaysFalsy - | SpecialFormType::Not - | SpecialFormType::Intersection - | SpecialFormType::TypeOf - | SpecialFormType::CallableTypeOf - | SpecialFormType::Top - | SpecialFormType::Bottom - | SpecialFormType::Callable - | SpecialFormType::TypingSelf - | SpecialFormType::Concatenate - | SpecialFormType::TypeGuard - | SpecialFormType::TypedDict - | SpecialFormType::TypeIs - | SpecialFormType::Protocol - | SpecialFormType::Generic - | SpecialFormType::NamedTuple => true, - } - } - - /// Return `Some(KnownClass)` if this special form is an alias - /// to a standard library class. - pub(super) const fn aliased_stdlib_class(self) -> Option { - match self { - Self::List => Some(KnownClass::List), - Self::Dict => Some(KnownClass::Dict), - Self::Set => Some(KnownClass::Set), - Self::FrozenSet => Some(KnownClass::FrozenSet), - Self::ChainMap => Some(KnownClass::ChainMap), - Self::Counter => Some(KnownClass::Counter), - Self::DefaultDict => Some(KnownClass::DefaultDict), - Self::Deque => Some(KnownClass::Deque), - Self::OrderedDict => Some(KnownClass::OrderedDict), - Self::Tuple => Some(KnownClass::Tuple), - Self::Type => Some(KnownClass::Type), - - Self::AlwaysFalsy - | Self::AlwaysTruthy - | Self::Annotated - | Self::Bottom - | Self::CallableTypeOf - | Self::ClassVar - | Self::Concatenate - | Self::Final - | Self::Intersection - | Self::Literal - | Self::LiteralString - | Self::Never - | Self::NoReturn - | Self::Not - | Self::ReadOnly - | Self::Required - | Self::TypeAlias - | Self::TypeGuard - | Self::NamedTuple - | Self::NotRequired - | Self::Optional - | Self::Top - | Self::TypeIs - | Self::TypedDict - | Self::TypingSelf - | Self::Union - | Self::Unknown - | Self::TypeOf - | Self::Any - // `typing.Callable` is an alias to `collections.abc.Callable`, - // but they're both the same `SpecialFormType` in our model, - // and neither is a class in typeshed (even though the `collections.abc` one is at runtime) - | Self::Callable - | Self::Protocol - | Self::Generic - | Self::Unpack => None, - } - } - /// Return `true` if this special form is valid as the second argument /// to `issubclass()` and `isinstance()` calls. pub(super) const fn is_valid_isinstance_target(self) -> bool { match self { Self::Callable - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::FrozenSet - | Self::Dict - | Self::List - | Self::OrderedDict - | Self::Set + | Self::LegacyStdlibAlias(_) | Self::Tuple | Self::Type | Self::Protocol @@ -463,21 +496,17 @@ impl SpecialFormType { | Self::Annotated | Self::Bottom | Self::CallableTypeOf - | Self::ClassVar + | Self::TypeQualifier(_) | Self::Concatenate - | Self::Final | Self::Intersection | Self::Literal | Self::LiteralString | Self::Never | Self::NoReturn | Self::Not - | Self::ReadOnly - | Self::Required | Self::TypeAlias | Self::TypeGuard | Self::NamedTuple - | Self::NotRequired | Self::Optional | Self::Top | Self::TypeIs @@ -505,27 +534,27 @@ impl SpecialFormType { SpecialFormType::Tuple => "Tuple", SpecialFormType::Type => "Type", SpecialFormType::TypingSelf => "Self", - SpecialFormType::Final => "Final", - SpecialFormType::ClassVar => "ClassVar", + SpecialFormType::TypeQualifier(TypeQualifier::Final) => "Final", + SpecialFormType::TypeQualifier(TypeQualifier::ClassVar) => "ClassVar", SpecialFormType::Callable => "Callable", SpecialFormType::Concatenate => "Concatenate", SpecialFormType::Unpack => "Unpack", - SpecialFormType::Required => "Required", - SpecialFormType::NotRequired => "NotRequired", + SpecialFormType::TypeQualifier(TypeQualifier::Required) => "Required", + SpecialFormType::TypeQualifier(TypeQualifier::NotRequired) => "NotRequired", SpecialFormType::TypeAlias => "TypeAlias", SpecialFormType::TypeGuard => "TypeGuard", SpecialFormType::TypedDict => "TypedDict", SpecialFormType::TypeIs => "TypeIs", - SpecialFormType::List => "List", - SpecialFormType::Dict => "Dict", - SpecialFormType::DefaultDict => "DefaultDict", - SpecialFormType::Set => "Set", - SpecialFormType::FrozenSet => "FrozenSet", - SpecialFormType::Counter => "Counter", - SpecialFormType::Deque => "Deque", - SpecialFormType::ChainMap => "ChainMap", - SpecialFormType::OrderedDict => "OrderedDict", - SpecialFormType::ReadOnly => "ReadOnly", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::List) => "List", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Dict) => "Dict", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) => "DefaultDict", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Set) => "Set", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet) => "FrozenSet", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Counter) => "Counter", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Deque) => "Deque", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap) => "ChainMap", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) => "OrderedDict", + SpecialFormType::TypeQualifier(TypeQualifier::ReadOnly) => "ReadOnly", SpecialFormType::Unknown => "Unknown", SpecialFormType::AlwaysTruthy => "AlwaysTruthy", SpecialFormType::AlwaysFalsy => "AlwaysFalsy", @@ -555,30 +584,20 @@ impl SpecialFormType { | SpecialFormType::Tuple | SpecialFormType::Type | SpecialFormType::TypingSelf - | SpecialFormType::Final - | SpecialFormType::ClassVar + | SpecialFormType::TypeQualifier(_) | SpecialFormType::Callable | SpecialFormType::Concatenate | SpecialFormType::Unpack - | SpecialFormType::Required - | SpecialFormType::NotRequired | SpecialFormType::TypeAlias | SpecialFormType::TypeGuard | SpecialFormType::TypedDict | SpecialFormType::TypeIs - | SpecialFormType::ReadOnly | SpecialFormType::Protocol | SpecialFormType::Generic | SpecialFormType::NamedTuple - | SpecialFormType::List - | SpecialFormType::Dict - | SpecialFormType::DefaultDict - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::Counter - | SpecialFormType::Deque - | SpecialFormType::ChainMap - | SpecialFormType::OrderedDict => &[KnownModule::Typing, KnownModule::TypingExtensions], + | SpecialFormType::LegacyStdlibAlias(_) => { + &[KnownModule::Typing, KnownModule::TypingExtensions] + } SpecialFormType::Unknown | SpecialFormType::AlwaysTruthy @@ -608,6 +627,128 @@ impl SpecialFormType { }) .map(TypeDefinition::SpecialForm) } + + /// Interpret this special form as an unparameterized type in a type-expression context. + /// + /// This is called for the "misc" special forms that are not aliases, type qualifiers, + /// `Tuple`, `Type`, or `Callable` (those are handled by their respective call sites). + pub(super) fn in_type_expression<'db>( + self, + db: &'db dyn Db, + scope_id: ScopeId<'db>, + typevar_binding_context: Option>, + ) -> Result, InvalidTypeExpressionError<'db>> { + match self { + Self::Never | Self::NoReturn => Ok(Type::Never), + Self::LiteralString => Ok(Type::literal_string()), + Self::Any => Ok(Type::any()), + Self::Unknown => Ok(Type::unknown()), + Self::AlwaysTruthy => Ok(Type::AlwaysTruthy), + Self::AlwaysFalsy => Ok(Type::AlwaysFalsy), + + // Special case: `NamedTuple` in a type expression is understood to describe the type + // `tuple[object, ...] & `. + // This isn't very principled (since at runtime, `NamedTuple` is just a function), + // but it appears to be what users often expect, and it improves compatibility with + // other type checkers such as mypy. + // See conversation in https://github.com/astral-sh/ruff/pull/19915. + Self::NamedTuple => Ok(IntersectionBuilder::new(db) + .positive_elements([ + Type::homogeneous_tuple(db, Type::object()), + KnownClass::NamedTupleLike.to_instance(db), + ]) + .build()), + + Self::TypingSelf => { + let index = semantic_index(db, scope_id.file(db)); + let Some(class) = nearest_enclosing_class(db, index, scope_id) else { + return Err(InvalidTypeExpressionError { + fallback_type: Type::unknown(), + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::InvalidType(Type::SpecialForm(self), scope_id) + ], + }); + }; + + Ok( + typing_self(db, scope_id, typevar_binding_context, class.into()) + .map(Type::TypeVar) + .unwrap_or(Type::SpecialForm(self)), + ) + } + // We ensure that `typing.TypeAlias` used in the expected position (annotating an + // annotated assignment statement) doesn't reach here. Using it in any other type + // expression is an error. + Self::TypeAlias => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::TypeAlias], + fallback_type: Type::unknown(), + }), + Self::TypedDict => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::TypedDict], + fallback_type: Type::unknown(), + }), + + Self::Literal | Self::Union | Self::Intersection => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresArguments(self) + ], + fallback_type: Type::unknown(), + }), + + Self::Protocol => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Protocol], + fallback_type: Type::unknown(), + }), + Self::Generic => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic], + fallback_type: Type::unknown(), + }), + + Self::Optional + | Self::Not + | Self::Top + | Self::Bottom + | Self::TypeOf + | Self::TypeIs + | Self::TypeGuard + | Self::Unpack + | Self::CallableTypeOf => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresOneArgument(self) + ], + fallback_type: Type::unknown(), + }), + + Self::Annotated | Self::Concatenate => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresTwoArguments(self) + ], + fallback_type: Type::unknown(), + }), + + // We treat `typing.Type` exactly the same as `builtins.type`: + SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), + SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), + SpecialFormType::Callable => Ok(Type::Callable(CallableType::unknown(db))), + SpecialFormType::LegacyStdlibAlias(alias) => Ok(alias.aliased_class().to_instance(db)), + SpecialFormType::TypeQualifier(qualifier) => { + let err = match qualifier { + TypeQualifier::Final | TypeQualifier::ClassVar => { + InvalidTypeExpression::TypeQualifier(qualifier) + } + TypeQualifier::ReadOnly + | TypeQualifier::NotRequired + | TypeQualifier::Required => { + InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) + } + }; + Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![err], + fallback_type: Type::unknown(), + }) + } + } + } } impl std::fmt::Display for SpecialFormType { @@ -620,3 +761,93 @@ impl std::fmt::Display for SpecialFormType { ) } } + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +pub enum LegacyStdlibAlias { + List, + Dict, + Set, + FrozenSet, + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, +} + +impl LegacyStdlibAlias { + pub(super) const fn alias_spec(self) -> AliasSpec { + let (class, expected_argument_number) = match self { + LegacyStdlibAlias::List => (KnownClass::List, 1), + LegacyStdlibAlias::Dict => (KnownClass::Dict, 2), + LegacyStdlibAlias::Set => (KnownClass::Set, 1), + LegacyStdlibAlias::FrozenSet => (KnownClass::FrozenSet, 1), + LegacyStdlibAlias::ChainMap => (KnownClass::ChainMap, 2), + LegacyStdlibAlias::Counter => (KnownClass::Counter, 1), + LegacyStdlibAlias::DefaultDict => (KnownClass::DefaultDict, 2), + LegacyStdlibAlias::Deque => (KnownClass::Deque, 1), + LegacyStdlibAlias::OrderedDict => (KnownClass::OrderedDict, 2), + }; + + AliasSpec { + class, + expected_argument_number, + } + } + + pub(super) const fn aliased_class(self) -> KnownClass { + self.alias_spec().class + } +} + +impl From for SpecialFormType { + fn from(value: LegacyStdlibAlias) -> Self { + SpecialFormType::LegacyStdlibAlias(value) + } +} + +impl std::fmt::Display for LegacyStdlibAlias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + SpecialFormType::from(*self).fmt(f) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +pub enum TypeQualifier { + ReadOnly, + Final, + ClassVar, + Required, + NotRequired, +} + +impl From for SpecialFormType { + fn from(value: TypeQualifier) -> Self { + SpecialFormType::TypeQualifier(value) + } +} + +impl From for TypeQualifiers { + fn from(value: TypeQualifier) -> Self { + match value { + TypeQualifier::ReadOnly => TypeQualifiers::READ_ONLY, + TypeQualifier::Final => TypeQualifiers::FINAL, + TypeQualifier::ClassVar => TypeQualifiers::CLASS_VAR, + TypeQualifier::Required => TypeQualifiers::REQUIRED, + TypeQualifier::NotRequired => TypeQualifiers::NOT_REQUIRED, + } + } +} + +impl std::fmt::Display for TypeQualifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + SpecialFormType::from(*self).fmt(f) + } +} + +/// Information regarding the [`KnownClass`] a [`LegacyStdlibAlias`] refers to. +#[derive(Debug)] +pub(super) struct AliasSpec { + pub(super) class: KnownClass, + pub(super) expected_argument_number: usize, +} From 058da1797076e1f87aa3754a1eae046da1e91865 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 24 Feb 2026 08:50:52 -0500 Subject: [PATCH 068/261] [ty] Fix inlay hints for starred unpacking targets (#23454) ## Summary Closes https://github.com/astral-sh/ty/issues/575. --- crates/ty_ide/src/inlay_hints.rs | 67 +++++++++++++++++++ .../src/semantic_index/builder.rs | 7 +- .../src/types/infer/builder.rs | 6 ++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 7e73b8e1b96bc..9701abbccac25 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1268,6 +1268,73 @@ mod tests { "#); } + #[test] + fn test_starred_unpacked_tuple_assignment() { + let mut test = inlay_hint_test( + " + def foo(x: tuple[int, ...]): + (a, *b) = x + ", + ); + + assert_snapshot!(test.inlay_hints(), @r#" + + def foo(x: tuple[int, ...]): + (a[: int], *b[: list[int]]) = x + + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | ^^^ + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | + info: Source + --> main2.py:3:10 + | + 2 | def foo(x: tuple[int, ...]): + 3 | (a[: int], *b[: list[int]]) = x + | ^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2829:7 + | + 2828 | @disjoint_base + 2829 | class list(MutableSequence[_T]): + | ^^^^ + 2830 | """Built-in mutable sequence. + | + info: Source + --> main2.py:3:21 + | + 2 | def foo(x: tuple[int, ...]): + 3 | (a[: int], *b[: list[int]]) = x + | ^^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | ^^^ + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | + info: Source + --> main2.py:3:26 + | + 2 | def foo(x: tuple[int, ...]): + 3 | (a[: int], *b[: list[int]]) = x + | ^^^ + | + "#); + } + #[test] fn test_leading_underscore_variable_assignment_has_no_type_inlay_hint() { let mut test = inlay_hint_test( diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 8cb1948a85a0b..0aad5a0532a33 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -1471,9 +1471,10 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { )); Some(unpackable.as_current_assignment(unpack)) } - ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { - Some(unpackable.as_current_assignment(None)) - } + ast::Expr::Name(_) + | ast::Expr::Starred(_) + | ast::Expr::Attribute(_) + | ast::Expr::Subscript(_) => Some(unpackable.as_current_assignment(None)), _ => None, }; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 8411d86c198c5..a437e8b9decf2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -6593,6 +6593,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_definition(name); } + ast::Expr::Starred(ast::ExprStarred { + value: starred_value, + .. + }) => { + self.infer_target_impl(starred_value, value, infer_assigned_ty); + } ast::Expr::List(ast::ExprList { elts, .. }) | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { let assigned_ty = infer_assigned_ty.map(|f| f(self, TypeContext::default())); From f267aed0e42f9117fb913138005eb65b8c31511b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 24 Feb 2026 08:58:37 -0500 Subject: [PATCH 069/261] [ty] Fix panic for annotation pointing at leading whitespace (#23458) ## Summary When a diagnostic annotation pointed at indentation (e.g., an "unexpected indentation" error), `whitespace_left` could exceed `span_left`, causing a subtraction overflow in `Margin::compute`. Closes https://github.com/astral-sh/ty/issues/836. --- .../src/renderer/margin.rs | 10 ++++++++-- crates/ruff_annotate_snippets/tests/formatter.rs | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/margin.rs b/crates/ruff_annotate_snippets/src/renderer/margin.rs index 59bd550745dcc..40e94e504887b 100644 --- a/crates/ruff_annotate_snippets/src/renderer/margin.rs +++ b/crates/ruff_annotate_snippets/src/renderer/margin.rs @@ -41,9 +41,15 @@ impl Margin { // | ^^^^^^^^^ // ``` + let whitespace_left = whitespace_left.saturating_sub(ELLIPSIS_PASSING); + let span_left = span_left.saturating_sub(ELLIPSIS_PASSING); + let mut m = Margin { - whitespace_left: whitespace_left.saturating_sub(ELLIPSIS_PASSING), - span_left: span_left.saturating_sub(ELLIPSIS_PASSING), + // When an annotation points at leading whitespace (e.g. an indentation error), + // `whitespace_left` can exceed `span_left`. Clamp it so that trimming whitespace + // never hides the leftmost annotation. + whitespace_left: min(whitespace_left, span_left), + span_left, span_right: span_right + ELLIPSIS_PASSING, computed_left: 0, computed_right: 0, diff --git a/crates/ruff_annotate_snippets/tests/formatter.rs b/crates/ruff_annotate_snippets/tests/formatter.rs index c150aa54386a8..540b0b590bcab 100644 --- a/crates/ruff_annotate_snippets/tests/formatter.rs +++ b/crates/ruff_annotate_snippets/tests/formatter.rs @@ -1029,3 +1029,19 @@ error let renderer = Renderer::plain().term_width(18).cut_indicator("…"); assert_data_eq!(renderer.render(input).to_string(), expected); } + +#[test] +fn leading_nbsp_no_overflow() { + // Regression test: an annotation pointing at leading whitespace caused a + // subtraction overflow in Margin::compute because `label_right` can be less + // than `whitespace_left`. See https://github.com/astral-sh/ty/issues/836 + let source = " \u{00A0} 'betting_env.datastructure.team_lineup.Tamheet.get_latest': ( 'dataStrcuture/team_lineup.htm#teamsheet.getlatest',"; + let input = Level::Error.title("test").snippet( + Snippet::source(source) + .line_start(1) + .annotation(Level::Error.span(0..1)), + ); + // Should not panic. + let renderer = Renderer::plain(); + let _ = renderer.render(input).to_string(); +} From 06f98ee01804665d631416abbf6e7845430d8288 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 24 Feb 2026 15:06:26 +0100 Subject: [PATCH 070/261] [ty] Narrow mapping patterns in `match` like `isinstance(Mapping)` (#23462) ## Summary Fixes https://github.com/astral-sh/ty/issues/2865 Make mapping patterns in `match` narrow types consistently with `isinstance(x, Mapping)`, including reachability behavior for match arms. ## Test Plan Validated in mdtests: - `case {}` narrows `dict | int` the same way as `isinstance(x, Mapping)` - the fallback after `case {}` excludes mapping-like values - the fallback after `case {"k": _}` does not over-narrow away mappings --------- Co-authored-by: David Peter --- .../resources/mdtest/narrow/match.md | 33 +++++++++++++++++++ .../src/semantic_index/builder.rs | 9 +++++ .../src/semantic_index/predicate.rs | 1 + .../reachability_constraints.rs | 29 ++++++++++++++-- crates/ty_python_semantic/src/types/narrow.rs | 26 +++++++++++++++ 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index d18d48b2b745c..ae60b97124491 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -118,6 +118,39 @@ def f(x: Covariant[int]): assert_never(x) ``` +## Mapping patterns + +```py +from collections.abc import Mapping + +def test_isinstance(x: dict | int) -> None: + if isinstance(x, Mapping): + reveal_type(x) # revealed: dict[Unknown, Unknown] | (int & Top[Mapping[Unknown, object]]) + else: + reveal_type(x) # revealed: int & ~Top[Mapping[Unknown, object]] + +def test_match(x: dict | int) -> None: + match x: + case {}: + reveal_type(x) # revealed: dict[Unknown, Unknown] | (int & Top[Mapping[Unknown, object]]) + case _: + reveal_type(x) # revealed: int & ~Top[Mapping[Unknown, object]] + +def test_match_double_star(x: dict | int) -> None: + match x: + case {**rest}: + reveal_type(x) # revealed: dict[Unknown, Unknown] | (int & Top[Mapping[Unknown, object]]) + case _: + reveal_type(x) # revealed: int & ~Top[Mapping[Unknown, object]] + +def test_match_refutable(x: dict | int) -> None: + match x: + case {"k": _}: + reveal_type(x) # revealed: dict[Unknown, Unknown] | (int & Top[Mapping[Unknown, object]]) + case _: + reveal_type(x) # revealed: dict[Unknown, Unknown] | int +``` + ## Value patterns Value patterns are evaluated by equality, which is overridable. Therefore successfully matching on diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 0aad5a0532a33..cebd4d58c7b3f 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -1130,6 +1130,15 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { }, ) } + ast::Pattern::MatchMapping(pattern) => { + // `case {}` and `case {**rest}` match every mapping, while keyed mapping + // patterns are refutable (`case {"x": _}` may fail for some mappings). + PatternPredicateKind::Mapping(if pattern.keys.is_empty() { + ClassPatternKind::Irrefutable + } else { + ClassPatternKind::Refutable + }) + } ast::Pattern::MatchOr(pattern) => { let predicates = pattern .patterns diff --git a/crates/ty_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs index 528e8143385a3..39a2b2fbeaa9c 100644 --- a/crates/ty_python_semantic/src/semantic_index/predicate.rs +++ b/crates/ty_python_semantic/src/semantic_index/predicate.rs @@ -135,6 +135,7 @@ pub(crate) enum PatternPredicateKind<'db> { Value(Expression<'db>), Or(Vec>), Class(Expression<'db>, ClassPatternKind), + Mapping(ClassPatternKind), As(Option>>, Option), Unsupported, } diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 20baeaa42df7d..27aab10a1358c 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -209,8 +209,8 @@ use crate::semantic_index::predicate::{ Predicates, ScopedPredicateId, }; use crate::types::{ - CallableTypes, IntersectionBuilder, NarrowingConstraint, Truthiness, Type, TypeContext, - UnionBuilder, UnionType, infer_expression_type, infer_narrowing_constraint, + CallableTypes, IntersectionBuilder, KnownClass, NarrowingConstraint, Truthiness, Type, + TypeContext, UnionBuilder, UnionType, infer_expression_type, infer_narrowing_constraint, }; /// A ternary formula that defines under what conditions a binding is visible. (A ternary formula @@ -330,6 +330,10 @@ fn singleton_to_type(db: &dyn Db, singleton: ruff_python_ast::Singleton) -> Type ty } +fn mapping_pattern_type(db: &dyn Db) -> Type<'_> { + KnownClass::Mapping.to_instance(db).top_materialization(db) +} + /// Turn a `match` pattern kind into a type that represents the set of all values that would definitely /// match that pattern. fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>) -> Type<'db> { @@ -356,6 +360,13 @@ fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>) Type::Never } } + PatternPredicateKind::Mapping(kind) => { + if kind.is_irrefutable() { + mapping_pattern_type(db) + } else { + Type::Never + } + } PatternPredicateKind::Or(predicates) => { UnionType::from_elements(db, predicates.iter().map(|p| pattern_kind_to_type(db, p))) } @@ -1050,6 +1061,20 @@ impl ReachabilityConstraints { } }) } + PatternPredicateKind::Mapping(kind) => { + let mapping_ty = mapping_pattern_type(db); + if subject_ty.is_subtype_of(db, mapping_ty) { + if kind.is_irrefutable() { + Truthiness::AlwaysTrue + } else { + Truthiness::Ambiguous + } + } else if subject_ty.is_disjoint_from(db, mapping_ty) { + Truthiness::AlwaysFalse + } else { + Truthiness::Ambiguous + } + } PatternPredicateKind::As(pattern, _) => pattern .as_deref() .map(|p| Self::analyze_single_pattern_predicate_kind(db, p, subject_ty)) diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index dc98dbf579534..669da9570c1ce 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -588,6 +588,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { PatternPredicateKind::Class(cls, kind) => { self.evaluate_match_pattern_class(subject, *cls, *kind, is_positive) } + PatternPredicateKind::Mapping(kind) => { + self.evaluate_match_pattern_mapping(subject, *kind, is_positive) + } PatternPredicateKind::Value(expr) => { self.evaluate_match_pattern_value(subject, *expr, is_positive) } @@ -1570,6 +1573,29 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { )])) } + fn evaluate_match_pattern_mapping( + &mut self, + subject: Expression<'db>, + kind: ClassPatternKind, + is_positive: bool, + ) -> Option> { + if !kind.is_irrefutable() && !is_positive { + return None; + } + + let subject = PlaceExpr::try_from_expr(subject.node_ref(self.db, self.module))?; + let place = self.expect_place(&subject); + + let mapping_type = ClassInfoConstraintFunction::IsInstance + .generate_constraint(self.db, KnownClass::Mapping.to_class_literal(self.db))? + .negate_if(self.db, !is_positive); + + Some(NarrowingConstraints::from_iter([( + place, + NarrowingConstraint::intersection(mapping_type), + )])) + } + fn evaluate_match_pattern_value( &mut self, subject: Expression<'db>, From ac33b19878469d168a587397fb700dc24c457793 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Tue, 24 Feb 2026 11:30:06 -0500 Subject: [PATCH 071/261] [ty] Avoid dictionary key narrowing for multi-target assignments (#23523) The example below currently panics. ```py x = y = { "a": 1 } ``` We attempt to synthesize places for `x["a"]` and `y["a"]` and associate them with a definition for the key-value assignment. However, the definition is uniquely identified by the AST node for `"a"`, causing a panic. This PR avoids narrowing for multi-target assignments. However, it looks like we do have internal support for associating multiple definitions with a single AST node, but we currently panic if it ever happens. I'm not sure if the assert is just enforcing our current behavior, or there's some other reason we avoid this? Resolves https://github.com/astral-sh/ty/issues/2866. --- .../mdtest/literal/collections/dictionary.md | 5 +++++ .../src/semantic_index/builder.rs | 17 +++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md index 5716d0415053c..9a1b077e0e23b 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -116,4 +116,9 @@ reveal_type(x5["a"]) # revealed: Literal[1] reveal_type(x5["b"]) # revealed: dict[str, int | TD] reveal_type(x5["b"]["c"]) # revealed: Literal[2] reveal_type(x5["b"]["d"]) # revealed: TD + +x6 = x7 = {"a": 1} +# TODO: This should reveal `Literal[1]`. +reveal_type(x6["a"]) # revealed: Unknown | int +reveal_type(x7["a"]) # revealed: Unknown | int ``` diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index cebd4d58c7b3f..17fa4147f0625 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2,6 +2,7 @@ use std::cell::{OnceCell, RefCell}; use std::sync::Arc; use except_handlers::TryNodeContextStackManager; +use itertools::Itertools; use rustc_hash::{FxHashMap, FxHashSet}; use ruff_db::files::File; @@ -788,18 +789,22 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { // Creates a definition for each key-value assignment in the dictionary. // - // If there are multiple targets, a given key-value definition will be created multiple - // times for each target. + // If there are multiple targets, no definitions will be created. fn add_dict_key_assignment_definitions( &mut self, targets: impl IntoIterator + Copy, dict: &'ast ast::ExprDict, assignment: Definition<'db>, ) { - for target in targets { - if let Some(target) = MemberExprBuilder::visit_expr(target.into()) { - self.add_dict_key_assignment_definitions_impl(&target, dict, assignment); - } + // TODO: Although we synthesize place expressions for each dictionary key, the definition + // is still uniquely associated with the AST node of the key expression, and so multiple target + // places cannot refer to the same key. + let Ok(target) = targets.into_iter().exactly_one() else { + return; + }; + + if let Some(target) = MemberExprBuilder::visit_expr(target.into()) { + self.add_dict_key_assignment_definitions_impl(&target, dict, assignment); } } From 5849e54707230f3cbfcbc74f4f06f56e2ebdf1d4 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:54:57 -0500 Subject: [PATCH 072/261] [`pyupgrade`] Fix false positive for `TypeVar` default before 3.12 (`UP046`) (#23540) ## Summary This was reported on [Discord](https://discord.com/channels/1039017663004942429/1070132471699607623/1475883727782674585): ```py import abc import typing from typing import Generic import typing_extensions _Caps = typing_extensions.TypeVar("_Caps", bound=int | str, contravariant=True, default=int | str) class CameraConnection(typing.Generic[_Caps]): # okay pass class Camera(Generic[_Caps], metaclass=abc.ABCMeta): # UP046 false positive pass ``` In short, `UP046` was suggesting to use PEP-695 type parameters in the second class on Python versions before 3.13 despite `_Caps` using a `default`, which is only supported in PEP-695 type parameters on 3.13 and later. The fix looks unrelated at first glance, but this early return was preventing the diagnostic from being defused later for having a `default`. ## Test Plan New test derived from the report --- .../ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py | 4 ++++ .../rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py index aab7dce4d3b95..bc6ccdd2713fb 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py @@ -11,3 +11,7 @@ class DefaultTypeVar(Generic[T]): var: T + + +class KeywordArguments(Generic[T], metaclass=type): + var: T diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs index a06fd90095653..f8fe57674c9d0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs @@ -159,7 +159,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD // // because `find_generic` also finds the *first* Generic argument, this has the additional // benefit of bailing out with a diagnostic if multiple Generic arguments are present - if generic_idx != arguments.len() - 1 { + if generic_idx != arguments.args.len() - 1 { return; } From 784c518a17725675265beb225f2c863948df27e2 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 24 Feb 2026 09:15:05 -0800 Subject: [PATCH 073/261] [ty] add reviewbot config (#23533) ## Summary Adds reviewer pools config for reviewer assignment bot. Implementation lives in astral-bot. ## Test Plan ...no good way to test other than merge and try it, if the config looks good. --- .github/pr-assignee-pools.toml | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/pr-assignee-pools.toml diff --git a/.github/pr-assignee-pools.toml b/.github/pr-assignee-pools.toml new file mode 100644 index 0000000000000..1f789258f3fc5 --- /dev/null +++ b/.github/pr-assignee-pools.toml @@ -0,0 +1,35 @@ +[[pools]] +name = "ty-semantic" +paths = ["/crates/ty_python_semantic/**"] +reviewers = ["carljm", "AlexWaygood", "sharkdp", "dcreager", "ibraheemdev", "oconnor663"] + +[[pools]] +name = "ty-module-resolver" +paths = ["/crates/ty_module_resolver/**", "/crates/ty_site_packages/**"] +reviewers = ["carljm", "AlexWaygood", "MichaReiser", "BurntSushi"] + +[[pools]] +name = "ty-infra" +paths = [ + "/crates/ruff_db/**", + "/crates/ty_combine/**", + "/crates/ty_project/**", + "/crates/ty/**", + "/crates/ty_benchmark/**", +] +reviewers = ["carljm", "MichaReiser", "BurntSushi"] + +[[pools]] +name = "ty-wasm" +paths = ["/crates/ty_wasm/**"] +reviewers = ["MichaReiser", "BurntSushi"] + +[[pools]] +name = "ty-ide" +paths = ["/crates/ty_ide/**"] +reviewers = ["MichaReiser", "AlexWaygood", "BurntSushi", "dhruvmanila"] + +[[pools]] +name = "ty-server" +paths = ["/crates/ty_server/**"] +reviewers = ["MichaReiser", "BurntSushi", "dhruvmanila"] From 8710af0dcdd505b73e16c0b92d524c2e545aee86 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:31:28 +0900 Subject: [PATCH 074/261] [ty] lower `MAX_RECURSIVE_UNION_LITERALS` (#23521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This is a simpler approach to the performance issues mentioned in #23520. isort's profiling results suggested that #22794 added a slow-converging calculation rather than adding a specific hotspot to the type inferer. What makes type inference for loop variables more troublesome than other types of type inference is the calculation of reachability. For other types, such as implicit attribute type inference, reachability analysis is not performed for each attribute binding (reverted due to performance issues: https://github.com/astral-sh/ruff/pull/20128, https://github.com/astral-sh/ty/issues/2117). They are all treated as reachable. Loop variables perform this heavy calculation (omitting reachability analysis from the `LoopHeader` branch of `infer_loop_header_definition` and `place_from_bindings_impl` will significantly improve performance). It appears that slow convergence for one variable in a loop block will also slow down the inference of all other definitions in the block that depend on it. To alleviate the issue, this PR reduces the value of `MAX_RECURSIVE_UNION_LITERALS` from 10 to 5. This will result in faster convergence when the loop variable grows like `Literal[0, 1, ...]`. Local measurements show that this PR alone improved the isort inspection time by about 37%. I chose 5 as the new value because I felt it offered a good balance between type inference precision and performance, based on the following measurement results: | Threshold | Mean | vs 10 | |-----------|--------|---------| | 4 | 0.89s | -38% | | 5 | 0.91s | -37% | | 6 | 1.05s | -27% | | 7 | 1.06s | -27% | | 8 | 1.23s | -14% | | 9 | 1.26s | -13% | | 10 (current) | 1.43s | — | It has been observed that this PR and another mitigation, #23520, are compatible, resulting in a total performance recovery of about 40-50%. ## Test Plan N/A --- crates/ty_python_semantic/resources/mdtest/call/union.md | 6 +++--- crates/ty_python_semantic/src/types/builder.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index b14cc427a51ac..ddb56cb51a38b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -299,16 +299,16 @@ class RecursiveAttr2: self.i = 0 def update(self): - self.i = (self.i + 1) % 9 + self.i = (self.i + 1) % 4 -reveal_type(RecursiveAttr2().i) # revealed: Unknown | Literal[0, 1, 2, 3, 4, 5, 6, 7, 8] +reveal_type(RecursiveAttr2().i) # revealed: Unknown | Literal[0, 1, 2, 3] class RecursiveAttr3: def __init__(self): self.i = 0 def update(self): - self.i = (self.i + 1) % 10 + self.i = (self.i + 1) % 5 # Going beyond the MAX_RECURSIVE_UNION_LITERALS limit: reveal_type(RecursiveAttr3().i) # revealed: Unknown | int diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 233b75277f409..c779ab0c1f95d 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -256,7 +256,7 @@ impl RecursivelyDefined { /// If the value ​​is defined recursively, widening is performed from fewer literal elements, /// resulting in faster convergence of the fixed-point iteration. -const MAX_RECURSIVE_UNION_LITERALS: usize = 10; +const MAX_RECURSIVE_UNION_LITERALS: usize = 5; /// If the value ​​is defined non-recursively, the fixed-point iteration will converge in one go, /// so in principle we can have as many literal elements as we want, /// but to avoid unintended huge computational loads, we limit it to 256. From b2d856f71666e089099bf7e6e785f8a79e6aaceb Mon Sep 17 00:00:00 2001 From: Giancarlo Cicellyn Comneno <126195429+gcomneno@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:28:45 +0100 Subject: [PATCH 075/261] [`flake8-bandit`] Allow suspicious imports in `TYPE_CHECKING` blocks (`S401`-`S415`) (#23441) Fix false positives for S408/S409 when `xml.dom.minidom` or `xml.dom.pulldom` are imported inside `if TYPE_CHECKING:` blocks. Imports inside TYPE_CHECKING are not executed at runtime, so they should not trigger these Bandit-based security rules. Adds a dedicated fixture and snapshot test for the TYPE_CHECKING case. Refs #14901 --- .../test/fixtures/flake8_bandit/S408_type_checking.py | 4 ++++ crates/ruff_linter/src/rules/flake8_bandit/mod.rs | 1 + .../src/rules/flake8_bandit/rules/suspicious_imports.rs | 5 +++++ ...es__flake8_bandit__tests__S408_S408_type_checking.py.snap | 5 +++++ 4 files changed, 15 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_bandit/S408_type_checking.py create mode 100644 crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408_type_checking.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S408_type_checking.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S408_type_checking.py new file mode 100644 index 0000000000000..b65fbc0872319 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S408_type_checking.py @@ -0,0 +1,4 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from xml.dom.minidom import Element diff --git a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs index 23ac58a41396f..3630666e040b0 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs @@ -67,6 +67,7 @@ mod tests { #[test_case(Rule::SuspiciousXmlExpatImport, Path::new("S407.pyi"))] #[test_case(Rule::SuspiciousXmlMinidomImport, Path::new("S408.py"))] #[test_case(Rule::SuspiciousXmlMinidomImport, Path::new("S408.pyi"))] + #[test_case(Rule::SuspiciousXmlMinidomImport, Path::new("S408_type_checking.py"))] #[test_case(Rule::SuspiciousXmlPulldomImport, Path::new("S409.py"))] #[test_case(Rule::SuspiciousXmlPulldomImport, Path::new("S409.pyi"))] #[test_case(Rule::SuspiciousLxmlImport, Path::new("S410.py"))] diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs index acc978d0549ad..d099b7fe42223 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs @@ -370,6 +370,11 @@ pub(crate) fn suspicious_imports(checker: &Checker, stmt: &Stmt) { return; } + // Imports inside `if TYPE_CHECKING:` are not executed at runtime. + if checker.semantic().in_type_checking_block() { + return; + } + match stmt { Stmt::Import(ast::StmtImport { names, .. }) => { for name in names { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408_type_checking.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408_type_checking.py.snap new file mode 100644 index 0000000000000..d02cf20c1e0dc --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408_type_checking.py.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs +assertion_line: 98 +--- + From df313c22a15a162dc9d14bf7a1f07d48c9d2d8f5 Mon Sep 17 00:00:00 2001 From: kar-ganap Date: Tue, 24 Feb 2026 12:46:39 -0800 Subject: [PATCH 076/261] Avoid infinite loop between `I002` and `PYI025` (#23352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #20891. When `lint.isort.required-imports` includes `from collections.abc import Set` (unaliased) and PYI025 is enabled, the two rules conflict: I002 inserts the unaliased import, while PYI025 demands it be aliased as `AbstractSet`. This causes an infinite autofix loop ("Failed to converge after 100 iterations"). Rather than patching the import-insertion logic at the rule level, this PR rejects the contradictory configuration at startup in `Configuration::into_settings()`, following the existing pattern established by `conflicting_import_settings()`. The error message explains the conflict and suggests two resolutions: alias the required import as `AbstractSet`, or disable PYI025. ### Prior art PR #21115 took a different approach (modifying `add_required_imports.rs` and `imports.rs` to relax alias matching). As noted in review, the better fix is to "reject this configuration entirely at an earlier stage" since it doesn't make sense to simultaneously require an unaliased import and forbid it. This PR implements that suggestion directly. ## Test plan - 3 unit tests: conflicting config → error, aliased as `AbstractSet` → ok, PYI025 not enabled → ok - Reproduction case: `ruff check --config ruff.toml --unsafe-fixes --fix` now emits a clear configuration error instead of entering the infinite loop - `cargo test -p ruff_workspace` — all 26 tests pass --- crates/ruff/tests/cli/lint.rs | 42 +++++++++++++++++++ ...t_aliased_as_abstract_set_no_conflict.snap | 24 +++++++++++ ...ired_import_set_conflicts_with_pyi025.snap | 25 +++++++++++ ...import_set_without_pyi025_no_conflict.snap | 24 +++++++++++ crates/ruff_workspace/src/configuration.rs | 35 ++++++++++++++++ 5 files changed, 150 insertions(+) create mode 100644 crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_aliased_as_abstract_set_no_conflict.snap create mode 100644 crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_conflicts_with_pyi025.snap create mode 100644 crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_without_pyi025_no_conflict.snap diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 8134a9af18600..ab1778fc4b81f 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -2871,6 +2871,48 @@ fn flake8_import_convention_unused_aliased_import_no_conflict() { ); } +// https://github.com/astral-sh/ruff/issues/20891 +#[test] +fn required_import_set_conflicts_with_pyi025() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(r#"lint.isort.required-imports = ["from collections.abc import Set"]"#) + .args(["--select", "I002,PYI025"]) + .arg("-") + .pass_stdin("1") + ); +} + +// https://github.com/astral-sh/ruff/issues/20891 +#[test] +fn required_import_set_aliased_as_abstract_set_no_conflict() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(r#"lint.isort.required-imports = ["from collections.abc import Set as AbstractSet"]"#) + .args(["--select", "I002,PYI025"]) + .arg("-") + .pass_stdin("1") + ); +} + +// https://github.com/astral-sh/ruff/issues/20891 +#[test] +fn required_import_set_without_pyi025_no_conflict() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(r#"lint.isort.required-imports = ["from collections.abc import Set"]"#) + .args(["--select", "I002"]) + .arg("-") + .pass_stdin("1") + ); +} + // https://github.com/astral-sh/ruff/issues/19842 #[test] fn pyupgrade_up026_respects_isort_required_import_fix() { diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_aliased_as_abstract_set_no_conflict.snap b/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_aliased_as_abstract_set_no_conflict.snap new file mode 100644 index 0000000000000..6c0175e9bf294 --- /dev/null +++ b/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_aliased_as_abstract_set_no_conflict.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff/tests/cli/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - concise + - "--config" + - "lint.isort.required-imports = [\"from collections.abc import Set as AbstractSet\"]" + - "--select" + - "I002,PYI025" + - "-" + stdin: "1" +--- +success: false +exit_code: 1 +----- stdout ----- +-:1:1: I002 [*] Missing required import: `from collections.abc import Set as AbstractSet` +Found 1 error. +[*] 1 fixable with the `--fix` option. + +----- stderr ----- diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_conflicts_with_pyi025.snap b/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_conflicts_with_pyi025.snap new file mode 100644 index 0000000000000..3cc72b112438c --- /dev/null +++ b/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_conflicts_with_pyi025.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff/tests/cli/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - concise + - "--config" + - "lint.isort.required-imports = [\"from collections.abc import Set\"]" + - "--select" + - "I002,PYI025" + - "-" + stdin: "1" +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +ruff failed + Cause: Required import `from collections.abc import Set` specified in `lint.isort.required-imports` (I002) conflicts with `unaliased-collections-abc-set-import` (PYI025), which requires this import to be aliased as `AbstractSet`. + +Help: Either alias the required import (`from collections.abc import Set as AbstractSet`), or disable PYI025. diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_without_pyi025_no_conflict.snap b/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_without_pyi025_no_conflict.snap new file mode 100644 index 0000000000000..41b1dfbb6bb76 --- /dev/null +++ b/crates/ruff/tests/cli/snapshots/cli__lint__required_import_set_without_pyi025_no_conflict.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff/tests/cli/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - concise + - "--config" + - "lint.isort.required-imports = [\"from collections.abc import Set\"]" + - "--select" + - I002 + - "-" + stdin: "1" +--- +success: false +exit_code: 1 +----- stdout ----- +-:1:1: I002 [*] Missing required import: `from collections.abc import Set` +Found 1 error. +[*] 1 fixable with the `--fix` option. + +----- stderr ----- diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index c2c6857d70a30..5e1831cbfc0b8 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -254,6 +254,7 @@ impl Configuration { .unwrap_or_default(); conflicting_import_settings(&isort, &flake8_import_conventions)?; + conflicting_required_import_pyi025(&isort, &rules)?; let future_annotations = lint.future_annotations.unwrap_or_default(); @@ -1693,6 +1694,40 @@ fn conflicting_import_settings( Ok(()) } +/// Detect conflicts between I002 (missing-required-import) and PYI025 +/// (unaliased-collections-abc-set-import). +/// +/// If `required-imports` includes `from collections.abc import Set` (without +/// aliasing it as `AbstractSet`) and PYI025 is enabled, the configuration is +/// contradictory: I002 requires the unaliased import, while PYI025 forbids it. +fn conflicting_required_import_pyi025( + isort: &isort::settings::Settings, + rules: &RuleTable, +) -> Result<()> { + if !rules.enabled(Rule::UnaliasedCollectionsAbcSetImport) { + return Ok(()); + } + + for required_import in &isort.required_imports { + let qualified_name = required_import.qualified_name(); + if qualified_name.segments() == ["collections", "abc", "Set"] + && required_import.bound_name() != "AbstractSet" + { + return Err(anyhow!( + "Required import `from collections.abc import Set` specified in \ + `lint.isort.required-imports` (I002) conflicts with \ + `unaliased-collections-abc-set-import` (PYI025), which requires \ + this import to be aliased as `AbstractSet`.\n\n\ + Help: Either alias the required import \ + (`from collections.abc import Set as AbstractSet`), \ + or disable PYI025." + )); + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use std::str::FromStr; From 46f71fbf50063c99598ff0a7d1e5524a144eff66 Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:24:03 -0500 Subject: [PATCH 077/261] [`flake8-import-conventions`] Add missing conventions from upstream (`ICN001`, `ICN002`) (#21373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds missing import conventions from upstream flake8-import-conventions plugin. Specifically: - `ICN001`: Enforces `plotly.graph_objects` → `go` and `statsmodels.api` → `sm` - `ICN002`: Bans `geopandas` → `gpd` alias Fixes #21300 ## Problem Analysis Ruff's `ICN001` (unconventional-import-alias) and `ICN002` (banned-import-alias) rules were missing some conventions enforced by the upstream [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions) plugin: 1. **ICN001 missing conventions:** - `plotly.graph_objects` should be imported as `go` (IC008 in upstream) - `statsmodels.api` should be imported as `sm` (IC010 in upstream) 2. **ICN002 missing convention:** - `geopandas` should not be imported as `gpd` (IC002 in upstream) The root cause was that these conventions were not included in Ruff's default configuration. The `CONVENTIONAL_ALIASES` constant was missing the two ICN001 entries, and there was no default banned aliases configuration for ICN002. ## Approach 1. **Added missing ICN001 conventions:** - Added `("plotly.graph_objects", "go")` and `("statsmodels.api", "sm")` to `CONVENTIONAL_ALIASES` in `settings.rs` - Updated the default option string in `options.rs` to include these new aliases 2. **Added default banned aliases for ICN002:** - Created `default_banned_aliases()` function returning `geopandas` → `["gpd"]` - Updated `Settings::default()` to use `default_banned_aliases()` - Updated `try_into_settings()` in `options.rs` to use `default_banned_aliases()` when `banned_aliases` is None - Updated the default option string for `banned_aliases` in `options.rs` 3. **Added comprehensive tests:** - Created `missing_conventions.py` test fixture with cases for all three new conventions - Added `missing_conventions` test in `mod.rs` to verify the new conventions work correctly ## Test Plan Added new snapshot test `missing_conventions` that verifies: - ICN001 correctly flags `plotly.graph_objects` without alias and requires `go` - ICN001 correctly flags `statsmodels.api` without alias and requires `sm` - ICN002 correctly flags `geopandas as gpd` as banned - All existing tests continue to pass (10/10 tests passing) The fix has been manually verified to match the behavior of upstream flake8-import-conventions. --------- Co-authored-by: Brent Westbrook --- ...quires_python_no_tool_preview_enabled.snap | 6 ++- .../flake8_import_conventions/defaults.py | 14 ++++++ crates/ruff_linter/src/preview.rs | 7 ++- .../rules/flake8_import_conventions/mod.rs | 38 +++++++++++---- .../flake8_import_conventions/settings.rs | 46 ++++++++++++++++-- ..._conventions__tests__defaults_preview.snap | 47 +++++++++++++++++++ crates/ruff_workspace/src/configuration.rs | 6 ++- crates/ruff_workspace/src/options.rs | 11 +++-- 8 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults_preview.snap diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 3590199c4ddc0..885ea0a77fa49 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -146,14 +146,18 @@ linter.flake8_import_conventions.aliases = { pandas = pd, panel = pn, plotly.express = px, + plotly.graph_objects = go, polars = pl, pyarrow = pa, seaborn = sns, + statsmodels.api = sm, tensorflow = tf, tkinter = tk, xml.etree.ElementTree = ET, } -linter.flake8_import_conventions.banned_aliases = {} +linter.flake8_import_conventions.banned_aliases = { + geopandas = [gpd], +} linter.flake8_import_conventions.banned_from = [] linter.flake8_pytest_style.fixture_parentheses = false linter.flake8_pytest_style.parametrize_names_type = tuple diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/defaults.py b/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/defaults.py index 0342d4203495f..52a1163e5f433 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/defaults.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/defaults.py @@ -30,3 +30,17 @@ def conventional_aliases(): import seaborn as sns import tkinter as tk import networkx as nx + + +# ICN001: plotly.graph_objects should be imported as go +import plotly.graph_objects # should require alias +import plotly.graph_objects as go # ok + +# ICN001: statsmodels.api should be imported as sm +import statsmodels.api # should require alias +import statsmodels.api as sm # ok + +# ICN002: geopandas should not be imported as gpd +import geopandas as gpd # banned +import geopandas # ok +import geopandas as gdf # ok diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 05d4159a3d90a..3cdb0e7d0f49f 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -5,7 +5,7 @@ //! which specific feature this preview check is for. Having named functions simplifies the promotion: //! Simply delete the function and let Rust tell you which checks you have to remove. -use crate::settings::LinterSettings; +use crate::settings::{LinterSettings, types::PreviewMode}; // Rule-specific behavior @@ -297,3 +297,8 @@ pub(crate) const fn is_resolve_string_annotation_pyi041_enabled(settings: &Linte pub(crate) const fn is_baseloader_safe_in_yaml_load_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/21373 +pub(crate) const fn is_expanded_import_conventions_enabled(preview: PreviewMode) -> bool { + preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs index 386e35035cc38..f36daca226e0d 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs @@ -9,11 +9,11 @@ mod tests { use anyhow::Result; use rustc_hash::{FxHashMap, FxHashSet}; - use crate::assert_diagnostics; use crate::registry::Rule; use crate::rules::flake8_import_conventions::settings::{BannedAliases, default_aliases}; - use crate::settings::LinterSettings; + use crate::settings::{LinterSettings, types::PreviewMode}; use crate::test::test_path; + use crate::{assert_diagnostics, assert_diagnostics_diff}; #[test] fn defaults() -> Result<()> { @@ -25,9 +25,31 @@ mod tests { Ok(()) } + #[test] + fn defaults_preview() -> Result<()> { + assert_diagnostics_diff!( + Path::new("flake8_import_conventions/defaults.py"), + &LinterSettings { + flake8_import_conventions: super::settings::Settings::new(PreviewMode::Disabled), + ..LinterSettings::for_rules([ + Rule::UnconventionalImportAlias, + Rule::BannedImportAlias + ]) + }, + &LinterSettings { + flake8_import_conventions: super::settings::Settings::new(PreviewMode::Enabled), + ..LinterSettings::for_rules([ + Rule::UnconventionalImportAlias, + Rule::BannedImportAlias + ]) + }, + ); + Ok(()) + } + #[test] fn custom() -> Result<()> { - let mut aliases = default_aliases(); + let mut aliases = default_aliases(PreviewMode::Disabled); aliases.extend(FxHashMap::from_iter([ ("dask.array".to_string(), "da".to_string()), ("dask.dataframe".to_string(), "dd".to_string()), @@ -53,7 +75,7 @@ mod tests { Path::new("flake8_import_conventions/custom_banned.py"), &LinterSettings { flake8_import_conventions: super::settings::Settings { - aliases: default_aliases(), + aliases: default_aliases(PreviewMode::Disabled), banned_aliases: FxHashMap::from_iter([ ( "typing".to_string(), @@ -87,7 +109,7 @@ mod tests { Path::new("flake8_import_conventions/custom_banned_from.py"), &LinterSettings { flake8_import_conventions: super::settings::Settings { - aliases: default_aliases(), + aliases: default_aliases(PreviewMode::Disabled), banned_aliases: FxHashMap::default(), banned_from: FxHashSet::from_iter([ "logging.config".to_string(), @@ -126,7 +148,7 @@ mod tests { #[test] fn override_defaults() -> Result<()> { - let mut aliases = default_aliases(); + let mut aliases = default_aliases(PreviewMode::Disabled); aliases.extend(FxHashMap::from_iter([( "numpy".to_string(), "nmp".to_string(), @@ -149,7 +171,7 @@ mod tests { #[test] fn from_imports() -> Result<()> { - let mut aliases = default_aliases(); + let mut aliases = default_aliases(PreviewMode::Disabled); aliases.extend(FxHashMap::from_iter([ ("xml.dom.minidom".to_string(), "md".to_string()), ( @@ -185,7 +207,7 @@ mod tests { #[test] fn same_name() -> Result<()> { - let mut aliases = default_aliases(); + let mut aliases = default_aliases(PreviewMode::Disabled); aliases.extend(FxHashMap::from_iter([( "django.conf.settings".to_string(), "settings".to_string(), diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs index f00406de4a181..7574d1f2d5c3f 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; use ruff_macros::CacheKey; use crate::display_settings; +use crate::preview::is_expanded_import_conventions_enabled; +use crate::settings::types::PreviewMode; const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[ ("altair", "alt"), @@ -28,6 +30,9 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[ ("xml.etree.ElementTree", "ET"), ]; +const PREVIEW_ALIASES: &[(&str, &str)] = + &[("plotly.graph_objects", "go"), ("statsmodels.api", "sm")]; + #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -66,18 +71,49 @@ pub struct Settings { pub banned_from: FxHashSet, } -pub fn default_aliases() -> FxHashMap { - CONVENTIONAL_ALIASES +pub fn default_aliases(preview: PreviewMode) -> FxHashMap { + let mut aliases = CONVENTIONAL_ALIASES .iter() .map(|(k, v)| ((*k).to_string(), (*v).to_string())) - .collect::>() + .collect::>(); + + if is_expanded_import_conventions_enabled(preview) { + aliases.extend( + PREVIEW_ALIASES + .iter() + .map(|(k, v)| ((*k).to_string(), (*v).to_string())), + ); + } + + aliases +} + +pub fn default_banned_aliases(preview: PreviewMode) -> FxHashMap { + if is_expanded_import_conventions_enabled(preview) { + FxHashMap::from_iter([( + "geopandas".to_string(), + BannedAliases::from_iter(["gpd".to_string()]), + )]) + } else { + FxHashMap::default() + } +} + +impl Settings { + pub fn new(preview: PreviewMode) -> Self { + Self { + aliases: default_aliases(preview), + banned_aliases: default_banned_aliases(preview), + banned_from: FxHashSet::default(), + } + } } impl Default for Settings { fn default() -> Self { Self { - aliases: default_aliases(), - banned_aliases: FxHashMap::default(), + aliases: default_aliases(PreviewMode::Disabled), + banned_aliases: default_banned_aliases(PreviewMode::Disabled), banned_from: FxHashSet::default(), } } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults_preview.snap b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults_preview.snap new file mode 100644 index 0000000000000..b56ab7fca7c0f --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults_preview.snap @@ -0,0 +1,47 @@ +--- +source: crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs +--- +--- Linter settings --- ++ plotly.graph_objects = go, ++ statsmodels.api = sm, +-linter.flake8_import_conventions.banned_aliases = {} ++linter.flake8_import_conventions.banned_aliases = { ++ geopandas = [gpd], ++} + +--- Summary --- +Removed: 0 +Added: 3 + +--- Added --- +ICN001 `plotly.graph_objects` should be imported as `go` + --> defaults.py:36:8 + | +35 | # ICN001: plotly.graph_objects should be imported as go +36 | import plotly.graph_objects # should require alias + | ^^^^^^^^^^^^^^^^^^^^ +37 | import plotly.graph_objects as go # ok + | +help: Alias `plotly.graph_objects` to `go` + + +ICN001 `statsmodels.api` should be imported as `sm` + --> defaults.py:40:8 + | +39 | # ICN001: statsmodels.api should be imported as sm +40 | import statsmodels.api # should require alias + | ^^^^^^^^^^^^^^^ +41 | import statsmodels.api as sm # ok + | +help: Alias `statsmodels.api` to `sm` + + +ICN002 `geopandas` should not be imported as `gpd` + --> defaults.py:44:1 + | +43 | # ICN002: geopandas should not be imported as gpd +44 | import geopandas as gpd # banned + | ^^^^^^^^^^^^^^^^^^^^^^^ +45 | import geopandas # ok +46 | import geopandas as gdf # ok + | diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 5e1831cbfc0b8..f61e261eab3a3 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -249,9 +249,11 @@ impl Configuration { .unwrap_or_default(); let flake8_import_conventions = lint .flake8_import_conventions - .map(Flake8ImportConventionsOptions::try_into_settings) + .map(|options| options.try_into_settings(lint_preview)) .transpose()? - .unwrap_or_default(); + .unwrap_or_else(|| { + ruff_linter::rules::flake8_import_conventions::settings::Settings::new(lint_preview) + }); conflicting_import_settings(&isort, &flake8_import_conventions)?; conflicting_required_import_pyi025(&isort, &rules)?; diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index f7b77e286e861..eb54e489b924e 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -32,7 +32,7 @@ use ruff_linter::rules::{ pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, }; use ruff_linter::settings::types::{ - IdentifierPattern, Language, OutputFormat, PythonVersion, RequiredVersion, + IdentifierPattern, Language, OutputFormat, PreviewMode, PythonVersion, RequiredVersion, }; use ruff_linter::{RuleSelector, warn_user_once}; use ruff_macros::{CombineOptions, OptionsMetadata}; @@ -1689,13 +1689,14 @@ impl<'de> Deserialize<'de> for Alias { impl Flake8ImportConventionsOptions { pub fn try_into_settings( self, + preview: PreviewMode, ) -> anyhow::Result { let mut aliases: FxHashMap = match self.aliases { Some(options_aliases) => options_aliases .into_iter() .map(|(module, alias)| (module.into_string(), alias.into_string())) .collect(), - None => flake8_import_conventions::settings::default_aliases(), + None => flake8_import_conventions::settings::default_aliases(preview), }; if let Some(extend_aliases) = self.extend_aliases { aliases.extend( @@ -1716,9 +1717,13 @@ impl Flake8ImportConventionsOptions { normalized_aliases.insert(module, normalized_alias); } + let banned_aliases = self.banned_aliases.unwrap_or_else(|| { + flake8_import_conventions::settings::default_banned_aliases(preview) + }); + Ok(flake8_import_conventions::settings::Settings { aliases: normalized_aliases, - banned_aliases: self.banned_aliases.unwrap_or_default(), + banned_aliases, banned_from: self.banned_from.unwrap_or_default(), }) } From 85a6a44936f5aa2fde67c92aad06263e299b41cc Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 24 Feb 2026 19:20:27 -0800 Subject: [PATCH 078/261] [ty] fix equality and contains narrowing with PEP 695 type aliases (#23545) ## Summary Fixes https://github.com/astral-sh/ty/issues/2903. We didn't resolve PEP 695 type aliases before deciding if its a narrowable type for `in` or equality narrowing. ## Test Plan Added mdtests. --- .../mdtest/narrow/conditionals/eq.md | 19 +++++++ .../mdtest/narrow/conditionals/in.md | 55 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 18 +++--- crates/ty_python_semantic/src/types/narrow.rs | 6 ++ 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index a98c232c2bc0b..a5b302a0188f6 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -168,6 +168,25 @@ def _(flag1: bool, flag2: bool): reveal_type(x) # revealed: Literal[2] ``` +## `==` with PEP 695 alias to a union of literals + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal + +type Y = Literal[2, 3] + +def _(x: Literal[1, 2], y: Y): + if x == y: + reveal_type(x) # revealed: Literal[2] + else: + reveal_type(x) # revealed: Literal[1, 2] +``` + ## `!=` for non-single-valued types Only single-valued types should narrow the type: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index 660ef375e7571..00f82d2a4cec9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -42,6 +42,61 @@ def _(x: Literal["a", "b", "c", 1]): reveal_type(x) # revealed: Literal[1] ``` +## `in` for PEP 695 aliases + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal, assert_never + +type Foo = Literal["a", "b", "c", "d"] + +def _(x: Foo): + if x in ("a", "b"): + reveal_type(x) # revealed: Literal["a", "b"] + else: + reveal_type(x) # revealed: Literal["c", "d"] + +def _(x: Foo) -> str: + if x in ("a", "b"): + return "AB" + match x: + case "c": + return "C" + case "d": + return "D" + case _ as never: + assert_never(never) +``` + +## `in` for mixed PEP 695 aliases + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal + +type Foo = Literal["a", "b", "c"] | int + +def _(x: Foo): + if x in ("a", "b"): + reveal_type(x) # revealed: Literal["a", "b"] | int + else: + reveal_type(x) # revealed: Literal["c"] | int + +def _(x: Foo): + if x not in ("a", "c"): + reveal_type(x) # revealed: Literal["b"] | int + else: + reveal_type(x) # revealed: Literal["a", "c"] | int +``` + ## `in` for `str` and literal strings ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index c8bb86c34897b..5b155ce9441e0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1542,29 +1542,31 @@ impl<'db> Type<'db> { } pub(crate) fn is_union_of_single_valued(&self, db: &'db dyn Db) -> bool { - self.as_union().is_some_and(|union| { + let ty = self.resolve_type_alias(db); + ty.as_union().is_some_and(|union| { union.elements(db).iter().all(|ty| { ty.is_single_valued(db) || ty.is_bool(db) || ty.is_literal_string() || (ty.is_enum(db) && !ty.overrides_equality(db)) }) - }) || self.is_bool(db) - || self.is_literal_string() - || (self.is_enum(db) && !self.overrides_equality(db)) + }) || ty.is_bool(db) + || ty.is_literal_string() + || (ty.is_enum(db) && !ty.overrides_equality(db)) } pub(crate) fn is_union_with_single_valued(&self, db: &'db dyn Db) -> bool { - self.as_union().is_some_and(|union| { + let ty = self.resolve_type_alias(db); + ty.as_union().is_some_and(|union| { union.elements(db).iter().any(|ty| { ty.is_single_valued(db) || ty.is_bool(db) || ty.is_literal_string() || (ty.is_enum(db) && !ty.overrides_equality(db)) }) - }) || self.is_bool(db) - || self.is_literal_string() - || (self.is_enum(db) && !self.overrides_equality(db)) + }) || ty.is_bool(db) + || ty.is_literal_string() + || (ty.is_enum(db) && !ty.overrides_equality(db)) } /// Create a promotable string literal. diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 669da9570c1ce..ce140917656cf 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -763,6 +763,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } fn evaluate_expr_eq(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { + let rhs_ty = rhs_ty.resolve_type_alias(self.db); + // We can only narrow on equality checks against single-valued types. if rhs_ty.is_single_valued(self.db) || rhs_ty.is_union_of_single_valued(self.db) { // The fully-general (and more efficient) approach here would be to introduce a @@ -873,6 +875,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { // TODO `expr_in` and `expr_not_in` should perhaps be unified with `expr_eq` and `expr_ne`, // since `eq` and `ne` are equivalent to `in` and `not in` with only one element in the RHS. fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { + let lhs_ty = lhs_ty.resolve_type_alias(self.db); + if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) { rhs_ty .try_iterate(self.db) @@ -916,6 +920,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } fn evaluate_expr_not_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { + let lhs_ty = lhs_ty.resolve_type_alias(self.db); + let rhs_values = rhs_ty .try_iterate(self.db) .ok()? From 1e88391ac4ec8151e218d5ea10d251ed797c2978 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 25 Feb 2026 09:14:21 +0100 Subject: [PATCH 079/261] [ty] Add micro benchmarks to review-bot pool (#23549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary … so that we get auto-assigned reviewers on PRs like https://github.com/astral-sh/ruff/pull/23546 --- .github/pr-assignee-pools.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pr-assignee-pools.toml b/.github/pr-assignee-pools.toml index 1f789258f3fc5..0fb0ce0c547a6 100644 --- a/.github/pr-assignee-pools.toml +++ b/.github/pr-assignee-pools.toml @@ -16,6 +16,7 @@ paths = [ "/crates/ty_project/**", "/crates/ty/**", "/crates/ty_benchmark/**", + "/crates/ruff_benchmark/benches/ty*", ] reviewers = ["carljm", "MichaReiser", "BurntSushi"] From ab26ad1d2e844e79565f356c6b5f147d38661aa7 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:18:28 +0900 Subject: [PATCH 080/261] [ty] add benchmark for large union type narrowing (#23546) ## Summary From https://github.com/astral-sh/ruff/pull/23201#discussion_r2841921540 Let's merge this benchmark into main ahead of #23201 so that we can see the impact of the changes. ## Test Plan `benchmark_large_union_narrowing` added to `ruff_benchmark/ty.rs` --- crates/ruff_benchmark/benches/ty.rs | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index e6990de32ef2a..6f5a122cea19d 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -658,6 +658,70 @@ class E(Enum): }); } +/// Benchmark for narrowing a large union type through multiple match statements. +/// +/// This is extracted from egglog-python's `pretty.py`, where a ~30-class union type +/// (`AllDecls`) is narrowed by exhaustive match statements. +/// +/// Sample code structure: +/// ```python +/// from __future__ import annotations +/// from dataclasses import dataclass +/// +/// @dataclass +/// class C0: +/// value: int +/// ... +/// +/// AllDecls = C0 | C1 | ... +/// +/// def process(decl: AllDecls) -> None: +/// match decl: +/// case C0(): pass +/// ... +/// case _: pass +/// ``` +fn benchmark_large_union_narrowing(criterion: &mut Criterion) { + const NUM_CLASSES: usize = 30; + const NUM_MATCH_BRANCHES: usize = 29; + + setup_rayon(); + + let mut code = + "from __future__ import annotations\nfrom dataclasses import dataclass\n\n".to_string(); + + for i in 0..NUM_CLASSES { + writeln!(&mut code, "@dataclass\nclass C{i}:\n value: int\n").ok(); + } + + code.push_str("AllDecls = "); + for i in 0..NUM_CLASSES { + if i > 0 { + code.push_str(" | "); + } + write!(&mut code, "C{i}").ok(); + } + code.push_str("\n\n"); + + code.push_str("def process(decl: AllDecls) -> None:\n match decl:\n"); + for i in 0..NUM_MATCH_BRANCHES { + writeln!(&mut code, " case C{i}():\n pass",).ok(); + } + code.push_str(" case _:\n pass\n\n"); + + criterion.bench_function("ty_micro[large_union_narrowing]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + struct ProjectBenchmark<'a> { project: InstalledProject<'a>, fs: MemoryFileSystem, @@ -820,6 +884,7 @@ criterion_group!( benchmark_many_enum_members, benchmark_many_enum_members_2, benchmark_very_large_tuple, + benchmark_large_union_narrowing, ); criterion_group!(project, anyio, attrs, hydra, datetype); criterion_main!(check_file, micro, project); From cb4b794450696f420de15f0d05e1c8f65631f96d Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:26:04 +0900 Subject: [PATCH 081/261] [ty] cache the intersection of two types as a tracked function (#23547) ## Summary From https://github.com/astral-sh/ruff/pull/23201#discussion_r2841967698 This could bring a significant performance improvement to main, so I'm breaking it out as a standalone PR. ## Test Plan N/A --------- Co-authored-by: David Peter --- crates/ty_python_semantic/src/types.rs | 21 ++++++++++++++++++- .../src/types/constraints.rs | 2 +- .../ty_python_semantic/src/types/generics.rs | 2 +- .../src/types/infer/builder.rs | 6 ++++-- crates/ty_python_semantic/src/types/narrow.rs | 2 +- .../src/types/special_form.rs | 16 +++++++------- crates/ty_python_semantic/src/types/tuple.rs | 6 +++--- 7 files changed, 38 insertions(+), 17 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 5b155ce9441e0..7445afd06edb3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4436,7 +4436,7 @@ impl<'db> Type<'db> { .with_annotated_type(typevar_meta)]; // Intersect with `Any` for the return type to reflect the fact that the `dataclass()` // decorator adds methods to the class - let returns = IntersectionType::from_elements(db, [typevar_meta, Type::any()]); + let returns = IntersectionType::from_two_elements(db, typevar_meta, Type::any()); let signature = Signature::new_generic(Some(context), Parameters::new(db, parameters), returns); Binding::single(self, signature).into() @@ -12847,7 +12847,12 @@ pub(super) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized> } } +#[salsa::tracked] impl<'db> IntersectionType<'db> { + /// Create an intersection type `E1 & E2 & ... & En` from a list of (positive) elements. + /// + /// For performance reasons, consider using [`IntersectionType::from_two_elements`] if + /// the intersection is constructed from exactly two elements. pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> where I: IntoIterator, @@ -12858,6 +12863,20 @@ impl<'db> IntersectionType<'db> { .build() } + /// Create an intersection type `A & B` from two elements `A` and `B`. + #[salsa::tracked( + cycle_initial=|_, id, _, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { + result.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + IntersectionBuilder::new(db) + .positive_elements([a, b]) + .build() + } + /// Return a new `IntersectionType` instance with the positive and negative types sorted /// according to a canonical ordering, and other normalizations applied to each element as applicable. /// diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 97f570a50e3ca..162040260d10f 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -788,7 +788,7 @@ impl<'db> ConstrainedTypeVar<'db> { // (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂)) let lower = UnionType::from_elements(db, [self.lower(db), other.lower(db)]); - let upper = IntersectionType::from_elements(db, [self_upper, other_upper]); + let upper = IntersectionType::from_two_elements(db, self_upper, other_upper); // If `lower ≰ upper`, then the intersection is empty, since there is no type that is both // greater than `lower`, and less than `upper`. diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 509a3d1bf5be9..f280444440397 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -2158,7 +2158,7 @@ impl<'db> SpecializationBuilder<'db> { // check here. self.add_type_mapping( bound_typevar, - IntersectionType::from_elements(self.db, [bound, ty]), + IntersectionType::from_two_elements(self.db, bound, ty), polarity, f, ); diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index a437e8b9decf2..afc5c2eb33e6d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10826,7 +10826,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // different overloads provide different type context; unioning may be more // correct in those cases. *argument_type = argument_type - .map(|current| IntersectionType::from_elements(db, [inferred_ty, current])) + .map(|current| { + IntersectionType::from_two_elements(db, inferred_ty, current) + }) .or(Some(inferred_ty)); } @@ -11040,7 +11042,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .annotation .is_none_or(|tcx| ty.is_assignable_to(db, tcx)) { - *current = IntersectionType::from_elements(db, [*current, ty]); + *current = IntersectionType::from_two_elements(db, *current, ty); } }) .or_insert(ty); diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index ce140917656cf..e892e6f033166 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -1376,7 +1376,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { .or_insert(constraint); // Use the narrowed type for subsequent comparisons in a chain. - last_rhs_ty = Some(IntersectionType::from_elements(self.db, [rhs_ty, ty])); + last_rhs_ty = Some(IntersectionType::from_two_elements(self.db, rhs_ty, ty)); } else { last_rhs_ty = Some(rhs_ty); } diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index b5c89466069e4..5e6e3c9a5a8da 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -7,9 +7,10 @@ use crate::semantic_index::place::ScopedPlaceId; use crate::semantic_index::{ FileScopeId, definition::Definition, place_table, scope::ScopeId, semantic_index, use_def_map, }; +use crate::types::IntersectionType; use crate::types::{ - CallableType, IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, - TypeDefinition, TypeQualifiers, generics::typing_self, infer::nearest_enclosing_class, + CallableType, InvalidTypeExpression, InvalidTypeExpressionError, TypeDefinition, + TypeQualifiers, generics::typing_self, infer::nearest_enclosing_class, }; use ruff_db::files::File; use strum_macros::EnumString; @@ -652,12 +653,11 @@ impl SpecialFormType { // but it appears to be what users often expect, and it improves compatibility with // other type checkers such as mypy. // See conversation in https://github.com/astral-sh/ruff/pull/19915. - Self::NamedTuple => Ok(IntersectionBuilder::new(db) - .positive_elements([ - Type::homogeneous_tuple(db, Type::object()), - KnownClass::NamedTupleLike.to_instance(db), - ]) - .build()), + Self::NamedTuple => Ok(IntersectionType::from_two_elements( + db, + Type::homogeneous_tuple(db, Type::object()), + KnownClass::NamedTupleLike.to_instance(db), + )), Self::TypingSelf => { let index = semantic_index(db, scope_id.file(db)); diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index b32d4009ec89f..8cacff51d213e 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -2136,11 +2136,11 @@ impl<'db> TupleSpecBuilder<'db> { && suffix.len() == var.suffix_elements().len() { for (existing, new) in prefix.iter_mut().zip(var.prefix_elements()) { - *existing = IntersectionType::from_elements(db, [*existing, *new]); + *existing = IntersectionType::from_two_elements(db, *existing, *new); } - *variable = IntersectionType::from_elements(db, [*variable, var.variable()]); + *variable = IntersectionType::from_two_elements(db, *variable, var.variable()); for (existing, new) in suffix.iter_mut().zip(var.suffix_elements()) { - *existing = IntersectionType::from_elements(db, [*existing, *new]); + *existing = IntersectionType::from_two_elements(db, *existing, *new); } return Some(self); } From a4e8c9230582eefaa3af67abcfa484a81b4e4ab6 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 25 Feb 2026 13:23:12 +0100 Subject: [PATCH 082/261] [ty] Add tests for pydantic validators (#23556) ## Summary Add some tests that make sure that we understand stacked decorators and properly infer the first parameter of validator/serializer methods as being of type `type[Self]`. See https://github.com/astral-sh/ty/issues/2719#issuecomment-3958798609 for context. --- .../resources/mdtest/external/pydantic.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md index a8a55f3a47d7f..ed1f2ca41b178 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md @@ -46,3 +46,42 @@ reveal_type(product.id) # revealed: int reveal_type(product.name) # revealed: str reveal_type(product.internal_price_cent) # revealed: int ``` + +## Validator and serializer decorators with explicit `@classmethod` + +Pydantic [recommends](https://docs.pydantic.dev/latest/concepts/validators/#class-validators) using +an explicit `@classmethod` decorator below `@field_validator` / `@model_validator(mode="before")` / +`@field_serializer` to get proper type checking. The first parameter should be inferred as +`type[Self]`. ty does not support recognizing these functions as *implicit* class methods, so the +`@classmethod` decorator is required for correct type inference. + +```py +from pydantic import BaseModel, field_validator, model_validator, field_serializer + +class User(BaseModel): + name: str + + @field_validator("name") + @classmethod + def validate_name(cls, v: str) -> str: + reveal_type(cls) # revealed: type[Self@validate_name] + return v.strip() + + @model_validator(mode="before") + @classmethod + def validate_model_before(cls, values: dict) -> dict: + reveal_type(cls) # revealed: type[Self@validate_model_before] + return values + + @field_serializer("name") + @classmethod + def serialize_name(cls, v: str) -> str: + reveal_type(cls) # revealed: type[Self@serialize_name] + return v.upper() + + # No @classmethod for "after" validators: the first parameter should be inferred as "Self" + @model_validator(mode="after") + def validate_model_after(self) -> "User": + reveal_type(self) # revealed: Self@validate_model_after + return self +``` From ab232176f0e1c3f46f913374581f227f2dd696bb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 25 Feb 2026 12:46:27 +0000 Subject: [PATCH 083/261] [ty] Fix panic where we would incorrectly consider overloads in another file as belonging to a function in the file being checked (#21977) --- .../resources/mdtest/overloads.md | 83 +++++++++++++++++++ .../ty_python_semantic/src/types/function.rs | 9 ++ 2 files changed, 92 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index 4dda8e1055cdf..985288bd52222 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -812,3 +812,86 @@ class Sub2(Base): # error: [invalid-overload] def method(self, x: str) -> str: ... ``` + +### Regression: `def` statement shadows a non-`def` symbol with the same name + +We used to panic on snippets like these (see ), because +"iterating over the overloads" for the `def` statement would incorrectly list the overloads of the +imported function. + +`module.pyi`: + +```pyi +from typing import overload + +@overload +def f() -> int: ... +@overload +def f(x) -> str: ... +@overload +def g() -> int: ... +@overload +def g(x) -> str: ... +``` + +`main.py`: + +```py +import module + +foo = module.f + +# revealed: Overload[() -> int, (x) -> str] +reveal_type(foo) + +def foo(): ... + +# revealed: def foo() -> Unknown +reveal_type(foo) + +bar = module.g + +# revealed: Overload[() -> int, (x) -> str] +reveal_type(bar) + +@staticmethod +def bar(): ... + +# revealed: def bar() -> Unknown +reveal_type(bar) +``` + +### Regression: `def` statement shadows a non-`def` symbol with the same name, defined in the same scope + +This is an even more pathological version of the above test. This version used to fail in the same +way as the above snippet, but would only fail in a stub file, or in a `.py` file that had an +overloaded function without an implementation. (Note that this is not always invalid even in `.py` +files: we allow overloaded functions to omit the implementation function if they are decorated with +`@abstractmethod` or they are defined in `if TYPE_CHECKING` blocks.) + +```pyi +from typing import overload + +@overload +def h() -> int: ... +@overload +def h(x) -> str: ... + +baz = h + +# revealed: Overload[() -> int, (x) -> str] +reveal_type(baz) + +# This function is distinct from `h`, despite `h` originating +# from the same scope and being aliased to the same name +# in the same scope! +@overload +def baz(x, y) -> bytes: ... +@overload +def baz(x, y, z) -> list[str]: ... +def baz(x, y, z=None) -> bytes | list[str]: + return b"" + +# revealed: Overload[(x, y) -> bytes, (x, y, z) -> list[str]] +reveal_type(baz) +``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 20ac041d3a099..6cf2eba08a5eb 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -395,6 +395,15 @@ impl<'db> OverloadLiteral<'db> { return None; } + // These can both happen in edge cases where a definition created with a `def` + // statement shadows a non-`def` symbol with the same name. + if previous_overload.name(db) != self.name(db) { + return None; + } + if previous_overload.definition(db).scope(db) != scope { + return None; + } + Some(previous_literal) } From a83276d06966a36e895f26e86613064ea0c10147 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:18:35 +0900 Subject: [PATCH 084/261] [ty] add benchmark for large `isinstance` narrowing chain (#23559) --- crates/ruff_benchmark/benches/ty.rs | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index 6f5a122cea19d..cd3a005f21fb9 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -722,6 +722,61 @@ fn benchmark_large_union_narrowing(criterion: &mut Criterion) { }); } +/// Benchmark for narrowing through a long `isinstance` elif chain. +/// +/// This pattern is common in visitor-style dispatch code (e.g. koda-validate's +/// `generate_schema_validator`) where a base class parameter is narrowed through +/// many sequential `isinstance` checks. +/// +/// Sample code structure: +/// ```python +/// class Base: ... +/// class C0(Base): ... +/// class C1(Base): ... +/// ... +/// +/// def f(obj: Base) -> None: +/// if isinstance(obj, C0): +/// pass +/// elif isinstance(obj, C1): +/// pass +/// ... +/// ``` +fn benchmark_large_isinstance_narrowing(criterion: &mut Criterion) { + const NUM_CLASSES: usize = 50; + + setup_rayon(); + + let mut code = String::new(); + writeln!(&mut code, "class Base: ...").ok(); + for i in 0..NUM_CLASSES { + writeln!(&mut code, "class C{i}(Base): ...").ok(); + } + writeln!(&mut code).ok(); + + writeln!(&mut code, "def f(obj: Base) -> None:").ok(); + for i in 0..NUM_CLASSES { + if i == 0 { + writeln!(&mut code, " if isinstance(obj, C{i}):").ok(); + } else { + writeln!(&mut code, " elif isinstance(obj, C{i}):").ok(); + } + writeln!(&mut code, " pass").ok(); + } + + criterion.bench_function("ty_micro[large_isinstance_narrowing]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + struct ProjectBenchmark<'a> { project: InstalledProject<'a>, fs: MemoryFileSystem, @@ -885,6 +940,7 @@ criterion_group!( benchmark_many_enum_members_2, benchmark_very_large_tuple, benchmark_large_union_narrowing, + benchmark_large_isinstance_narrowing, ); criterion_group!(project, anyio, attrs, hydra, datetype); criterion_main!(check_file, micro, project); From 57a1eb2a41f8d435e9cd81feb7cc61dfb11458d4 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 25 Feb 2026 11:00:44 -0800 Subject: [PATCH 085/261] [`ruff`] Support file level noqa in `RUF102` (#23535) --- crates/ruff/tests/cli/lint.rs | 51 ------------------- ...warn_invalid_noqa_with_no_diagnostics.snap | 22 -------- .../resources/test/fixtures/ruff/RUF102_1.py | 8 +++ crates/ruff_linter/src/checkers/noqa.rs | 8 ++- crates/ruff_linter/src/noqa.rs | 30 +++++------ crates/ruff_linter/src/preview.rs | 5 ++ crates/ruff_linter/src/rules/ruff/mod.rs | 15 ++++-- .../src/rules/ruff/rules/invalid_rule_code.rs | 33 ++++++++---- ...ules__ruff__tests__RUF102_RUF102_1.py.snap | 4 ++ ..._code_external_rules_ruff__RUF102.py.snap} | 0 ...code_external_rules_ruff__RUF102_1.py.snap | 19 +++++++ 11 files changed, 88 insertions(+), 107 deletions(-) delete mode 100644 crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF102_1.py create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap => ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap} (100%) create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102_1.py.snap diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index ab1778fc4b81f..12b697a5ddbdc 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -1075,57 +1075,6 @@ include = ["*.ipy"] Ok(()) } -#[test] -fn warn_invalid_noqa_with_no_diagnostics() { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--isolated"]) - .arg("--select") - .arg("F401") - .arg("-") - .pass_stdin( - r#" -# ruff: noqa: AAA101 -print("Hello world!") -"# - ) - ); -} - -#[test] -fn file_noqa_external() -> Result<()> { - let fixture = CliTest::with_file( - "ruff.toml", - r#" -[lint] -external = ["AAA"] -"#, - )?; - - assert_cmd_snapshot!(fixture - .check_command() - .arg("--config") - .arg("ruff.toml") - .arg("-") - .pass_stdin(r#" -# flake8: noqa: AAA101, BBB102 -import os -"#), @" - success: false - exit_code: 1 - ----- stdout ----- - -:3:8: F401 [*] `os` imported but unused - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102 - "); - - Ok(()) -} - #[test] fn required_version_fails_to_parse() -> Result<()> { let fixture = CliTest::with_file( diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap b/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap deleted file mode 100644 index 1dfaffe5b38fd..0000000000000 --- a/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap +++ /dev/null @@ -1,22 +0,0 @@ ---- -source: crates/ruff/tests/cli/lint.rs -info: - program: ruff - args: - - check - - "--no-cache" - - "--output-format" - - concise - - "--isolated" - - "--select" - - F401 - - "-" - stdin: "\n# ruff: noqa: AAA101\nprint(\"Hello world!\")\n" ---- -success: true -exit_code: 0 ------ stdout ----- -All checks passed! - ------ stderr ----- -warning: Invalid rule code provided to `# ruff: noqa` at -:2: AAA101 diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF102_1.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF102_1.py new file mode 100644 index 0000000000000..c809118f4fd27 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF102_1.py @@ -0,0 +1,8 @@ +# Invalid file-level code +# ruff: noqa: INVALID123 + +# External file-level code +# ruff: noqa: V123 + +# Valid file-level code +# ruff: noqa: E402 diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index 7ba55e2294e76..83f026a462402 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -271,7 +271,13 @@ pub(crate) fn check_noqa( if context.is_rule_enabled(Rule::InvalidRuleCode) && !exemption.enumerates(Rule::InvalidRuleCode) { - ruff::rules::invalid_noqa_code(context, &noqa_directives, locator, &settings.external); + ruff::rules::invalid_noqa_code( + context, + &file_noqa_directives, + &noqa_directives, + locator, + &settings.external, + ); } ignored_diagnostics.sort_unstable(); diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index a324aef00ac04..8441bf943405a 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -277,24 +277,18 @@ impl<'a> FileNoqaDirectives<'a> { vec![] } Directive::Codes(codes) => { - codes.iter().filter_map(|code| { - let code = code.as_str(); - // Ignore externally-defined rules. - if external.iter().any(|external| code.starts_with(external)) { - return None; - } - - if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) - { - Some(rule) - } else { - #[expect(deprecated)] - let line = locator.compute_line_index(range.start()); - let path_display = relativize_path(path); - warn!("Invalid rule code provided to `# ruff: noqa` at {path_display}:{line}: {code}"); - None - } - }).collect() + codes + .iter() + .filter_map(|code| { + let code = code.as_str(); + // Ignore externally-defined rules. + if external.iter().any(|external| code.starts_with(external)) { + return None; + } + + Rule::from_code(get_redirect_target(code).unwrap_or(code)).ok() + }) + .collect() } }; diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 3cdb0e7d0f49f..d7531160c0baf 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -302,3 +302,8 @@ pub(crate) const fn is_baseloader_safe_in_yaml_load_enabled(settings: &LinterSet pub(crate) const fn is_expanded_import_conventions_enabled(preview: PreviewMode) -> bool { preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/23535 +pub(crate) const fn is_file_level_invalid_rule_code_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 552b8b1095ca2..939067fd24d41 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -121,6 +121,7 @@ mod tests { #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))] #[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))] + #[test_case(Rule::InvalidRuleCode, Path::new("RUF102_1.py"))] #[test_case(Rule::NonEmptyInitModule, Path::new("RUF067/modules/__init__.py"))] #[test_case(Rule::NonEmptyInitModule, Path::new("RUF067/modules/okay.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -497,16 +498,22 @@ mod tests { Ok(()) } - #[test] - fn invalid_rule_code_external_rules() -> Result<()> { + #[test_case(Path::new("ruff/RUF102.py"))] + #[test_case(Path::new("ruff/RUF102_1.py"))] + fn invalid_rule_code_external_rules(path: &Path) -> Result<()> { + let snapshot = format!( + "invalid_rule_code_external_rules_{}", + path.to_string_lossy(), + ); let diagnostics = test_path( - Path::new("ruff/RUF102.py"), + path, &settings::LinterSettings { external: vec!["V".to_string()], + preview: PreviewMode::Enabled, ..settings::LinterSettings::for_rule(Rule::InvalidRuleCode) }, )?; - assert_diagnostics!(diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs index 038a2d6a46a32..4988591910f39 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs @@ -4,8 +4,9 @@ use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::Locator; use crate::checkers::ast::LintContext; use crate::fix::edits::delete_comment; -use crate::noqa::{Code, Directive}; +use crate::noqa::{Code, Directive, FileNoqaDirectives}; use crate::noqa::{Codes, NoqaDirectives}; +use crate::preview::is_file_level_invalid_rule_code_enabled; use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; use crate::{AlwaysFixableViolation, Edit, Fix}; @@ -83,35 +84,45 @@ impl AlwaysFixableViolation for InvalidRuleCode { /// RUF102 for invalid noqa codes pub(crate) fn invalid_noqa_code( context: &LintContext, + file_noqa_directives: &FileNoqaDirectives, noqa_directives: &NoqaDirectives, locator: &Locator, external: &[String], ) { - for line in noqa_directives.lines() { - let Directive::Codes(directive) = &line.directive else { - continue; - }; - - let all_valid = directive + let check_codes = |codes: &Codes<'_>| { + let all_valid = codes .iter() .all(|code| code_is_valid(code.as_str(), external)); if all_valid { - continue; + return; } - let (valid_codes, invalid_codes): (Vec<_>, Vec<_>) = directive + let (valid_codes, invalid_codes): (Vec<_>, Vec<_>) = codes .iter() .partition(|&code| code_is_valid(code.as_str(), external)); if valid_codes.is_empty() { - all_codes_invalid_diagnostic(directive, invalid_codes, locator, context); + all_codes_invalid_diagnostic(codes, invalid_codes, locator, context); } else { for invalid_code in invalid_codes { - some_codes_are_invalid_diagnostic(directive, invalid_code, locator, context); + some_codes_are_invalid_diagnostic(codes, invalid_code, locator, context); + } + } + }; + + if is_file_level_invalid_rule_code_enabled(context.settings()) { + for line in file_noqa_directives.lines() { + if let Directive::Codes(codes) = &line.parsed_file_exemption { + check_codes(codes); } } } + for line in noqa_directives.lines() { + if let Directive::Codes(codes) = &line.directive { + check_codes(codes); + } + } } pub(crate) fn code_is_valid(code: &str, external: &[String]) -> bool { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap new file mode 100644 index 0000000000000..7f58cfd7246a3 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102_1.py.snap new file mode 100644 index 0000000000000..12581c2826e07 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102_1.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF102 [*] Invalid rule code in `# noqa`: INVALID123 + --> RUF102_1.py:2:1 + | +1 | # Invalid file-level code +2 | # ruff: noqa: INVALID123 + | ^^^^^^^^^^^^^^^^^^^^^^^^ +3 | +4 | # External file-level code + | +help: Add non-Ruff rule codes to the `lint.external` configuration option +help: Remove the `# noqa` comment +1 | # Invalid file-level code + - # ruff: noqa: INVALID123 +2 | +3 | # External file-level code +4 | # ruff: noqa: V123 From 85dac631b4bcb00effcd6e61a9c416e072cf3ea8 Mon Sep 17 00:00:00 2001 From: Karthik Date: Thu, 26 Feb 2026 00:35:50 +0530 Subject: [PATCH 086/261] [`pydocstyle`] Add rule `D420` to enforce docstring section ordering (#23537) Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../test/fixtures/pydocstyle/D420_google.py | 207 +++++++++++++++ .../test/fixtures/pydocstyle/D420_numpy.py | 241 ++++++++++++++++++ .../src/checkers/ast/analyze/definitions.rs | 2 + crates/ruff_linter/src/codes.rs | 1 + .../ruff_linter/src/rules/pydocstyle/mod.rs | 18 ++ .../src/rules/pydocstyle/rules/sections.rs | 229 ++++++++++++++++- .../src/rules/pydocstyle/settings.rs | 1 + ...ydocstyle__tests__D420_D420_google.py.snap | 78 ++++++ ...pydocstyle__tests__D420_D420_numpy.py.snap | 79 ++++++ ruff.schema.json | 2 + 10 files changed, 853 insertions(+), 5 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_google.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_numpy.py create mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D420_D420_google.py.snap create mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D420_D420_numpy.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_google.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_google.py new file mode 100644 index 0000000000000..1d19493ef2a4a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_google.py @@ -0,0 +1,207 @@ +"""Tests for D420 (section order) with Google convention.""" + + +def correct_order(): + """Summary. + + Args: + x: Description. + + Keyword Args: + y: Description. + + Returns: + int + + Yields: + int + + Raises: + ValueError + + Notes: + Some notes. + + Examples: + >>> correct_order() + """ + + +def returns_before_args(): + """Summary. + + Returns: + int + + Args: + x: Description. + """ + + +def raises_before_returns(): + """Summary. + + Raises: + ValueError + + Returns: + int + """ + + +def raises_before_args(): + """Summary. + + Raises: + ValueError + + Args: + x: Description. + + Returns: + int + """ + + +def single_section(): + """Summary. + + Returns: + int + """ + + +def correct_args_order(): + """Summary. + + Args: + x: Description. + + Keyword Args: + y: Description. + + Returns: + int + """ + + +# All arg-like sections share position 0 — no ordering enforced among them. +def keyword_args_before_args(): + """Summary. + + Keyword Args: + y: Description. + + Args: + x: Description. + """ + + +def arguments_alias(): + """Summary. + + Returns: + int + + Arguments: + x: Description. + """ + + +# Non-core sections (Notes, Examples, Attributes, etc.) are unordered. +# No diagnostic expected for any ordering among them. +def notes_before_raises_unordered(): + """Summary. + + Notes: + Some notes. + + Raises: + ValueError + """ + + +def unordered_sections_only(): + """Summary. + + Examples: + >>> func() + + Notes: + Some notes. + """ + + +def examples_before_returns(): + """Summary. + + Examples: + >>> func() + + Returns: + int + """ + + +def returns_then_unordered_then_args(): + """Summary. + + Returns: + int + + Notes: + Some notes. + + Args: + x: Description. + """ + + +class CorrectClassDocstring: + """Summary. + + Attributes: + x: Description. + + Examples: + >>> obj = CorrectClassDocstring() + """ + + +# Attributes and Examples are both unordered — no diagnostic. +class UnorderedClassDocstring: + """Summary. + + Examples: + >>> obj = UnorderedClassDocstring() + + Attributes: + x: Description. + """ + + +def _private_function_out_of_order(): + """Summary. + + Returns: + int + + Args: + x: Description. + """ + + +def no_sections(): + """Summary. + + This is a docstring with no recognized sections at all. + It just has some plain text that shouldn't trigger D420. + """ + + +def plain_text_with_section_like_words(): + """Summary. + + Returns the value of x. Notes that this is important. + This should not be detected as having sections. + """ diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_numpy.py new file mode 100644 index 0000000000000..4ef310ece57c3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_numpy.py @@ -0,0 +1,241 @@ +"""Tests for D420 (section order) with NumPy convention.""" + + +def correct_order(): + """Summary. + + Extended Summary + ---------------- + More details. + + Parameters + ---------- + x : int + Description. + + Returns + ------- + int + + Yields + ------ + int + + Receives + -------- + int + + Other Parameters + ---------------- + y : int + Description. + + Raises + ------ + ValueError + + Warns + ----- + UserWarning + + Warnings + -------- + Be careful. + + See Also + -------- + other_func + + Notes + ----- + Some notes. + + References + ---------- + [1] Reference. + + Examples + -------- + >>> correct_order() + + Attributes + ---------- + attr : int + + Methods + ------- + method + """ + + +def single_swap(): + """Summary. + + Notes + ----- + Some notes. + + Returns + ------- + int + """ + + +def multiple_out_of_order(): + """Summary. + + Examples + -------- + >>> func() + + Returns + ------- + int + + Notes + ----- + Some notes. + """ + + +def single_section(): + """Summary. + + Returns + ------- + int + """ + + +def notes_before_warns(): + """Summary. + + Notes + ----- + Some notes. + + Warns + ----- + UserWarning + """ + + +def other_params_alias(): + """Summary. + + Other Params + ------------ + y : int + Description. + + Parameters + ---------- + x : int + Description. + """ + + +def correct_partial(): + """Summary. + + Parameters + ---------- + x : int + Description. + + Returns + ------- + int + + Examples + -------- + >>> func() + """ + + +class CorrectClassDocstring: + """Summary. + + Attributes + ---------- + x : int + Description. + + Methods + ------- + do_something + """ + + +class WrongClassDocstring: + """Summary. + + Methods + ------- + do_something + + Attributes + ---------- + x : int + Description. + """ + + +def unrecognized_sections_only(): + """Summary. + + Custom Section + -------------- + Some content. + + Another Custom + -------------- + More content. + """ + + +def unrecognized_mixed_with_recognized(): + """Summary. + + Parameters + ---------- + x : int + Description. + + Custom Section + -------------- + Some content. + + Returns + ------- + int + """ + + +def _private_function_out_of_order(): + """Summary. + + Notes + ----- + Some notes. + + Returns + ------- + int + """ + + +def no_sections(): + """Summary. + + This is a docstring with no recognized sections at all. + It just has some plain text that shouldn't trigger D420. + """ + + +def plain_text_with_section_like_words(): + """Summary. + + Returns the value of x. Notes that this is important. + This should not be detected as having sections. + """ diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index 4a3fe560be3c0..68570cb27e1a8 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -72,6 +72,7 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::UnderIndentation, Rule::UndocumentedMagicMethod, Rule::UndocumentedParam, + Rule::IncorrectSectionOrder, Rule::UndocumentedPublicClass, Rule::UndocumentedPublicFunction, Rule::UndocumentedPublicInit, @@ -286,6 +287,7 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::MismatchedSectionUnderlineLength, Rule::OverindentedSectionUnderline, Rule::UndocumentedParam, + Rule::IncorrectSectionOrder, ]); if enforce_sections || enforce_pydoclint { let section_contexts = pydocstyle::helpers::get_section_contexts( diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 3d70edfc5d66d..4b0cfba291847 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -630,6 +630,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pydocstyle, "417") => rules::pydocstyle::rules::UndocumentedParam, (Pydocstyle, "418") => rules::pydocstyle::rules::OverloadWithDocstring, (Pydocstyle, "419") => rules::pydocstyle::rules::EmptyDocstring, + (Pydocstyle, "420") => rules::pydocstyle::rules::IncorrectSectionOrder, // pep8-naming (PEP8Naming, "801") => rules::pep8_naming::rules::InvalidClassName, diff --git a/crates/ruff_linter/src/rules/pydocstyle/mod.rs b/crates/ruff_linter/src/rules/pydocstyle/mod.rs index 2df67b2320146..948f770f0b336 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/mod.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/mod.rs @@ -202,6 +202,24 @@ mod tests { Ok(()) } + #[test_case(Convention::Numpy, Path::new("D420_numpy.py"))] + #[test_case(Convention::Google, Path::new("D420_google.py"))] + fn d420(convention: Convention, path: &Path) -> Result<()> { + let snapshot = format!("D420_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("pydocstyle").join(path).as_path(), + &settings::LinterSettings { + pydocstyle: Settings { + convention: Some(convention), + ..Settings::default() + }, + ..settings::LinterSettings::for_rule(Rule::IncorrectSectionOrder) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + #[test] fn d209_d400() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 379bb2bfdab65..88d1489f81333 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1348,8 +1348,102 @@ impl AlwaysFixableViolation for BlankLinesBetweenHeaderAndContent { } } +/// ## What it does +/// Checks for docstring sections that appear out of order. +/// +/// ## Why is this bad? +/// Docstring sections should follow the canonical ordering specified by the +/// docstring convention (NumPy or Google). Consistent ordering makes +/// docstrings easier to read and navigate. +/// +/// For the NumPy convention, all sections have a prescribed order per the +/// numpydoc style guide. For the Google convention, only the relative ordering +/// of `Args`, `Returns`/`Yields`, and `Raises` is enforced; all other sections +/// are unordered. +/// +/// ## Example +/// +/// Given `lint.pydocstyle.convention = "numpy"`: +/// +/// ```python +/// def func() -> int: +/// """Summary. +/// +/// Notes +/// ----- +/// Some notes. +/// +/// Returns +/// ------- +/// int +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def func() -> int: +/// """Summary. +/// +/// Returns +/// ------- +/// int +/// +/// Notes +/// ----- +/// Some notes. +/// """ +/// ``` +/// +/// Given `lint.pydocstyle.convention = "google"`: +/// +/// ```python +/// def func(x: int) -> int: +/// """Summary. +/// +/// Returns: +/// int +/// +/// Args: +/// x: Description. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def func(x: int) -> int: +/// """Summary. +/// +/// Args: +/// x: Description. +/// +/// Returns: +/// int +/// """ +/// ``` +/// +/// ## Options +/// - `lint.pydocstyle.convention` +/// +/// ## References +/// - [NumPy docstring standard](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods) +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] +pub(crate) struct IncorrectSectionOrder { + current: String, + previous: String, +} + +impl Violation for IncorrectSectionOrder { + #[derive_message_formats] + fn message(&self) -> String { + let IncorrectSectionOrder { current, previous } = self; + format!(r#"Section "{current}" appears after section "{previous}" but should be before it"#) + } +} + /// D212, D214, D215, D405, D406, D407, D408, D409, D410, D411, D412, D413, -/// D414, D416, D417 +/// D414, D416, D417, D420 pub(crate) fn sections( checker: &Checker, docstring: &Docstring, @@ -1357,11 +1451,23 @@ pub(crate) fn sections( convention: Option, ) { match convention { - Some(Convention::Google) => parse_google_sections(checker, docstring, section_contexts), - Some(Convention::Numpy) => parse_numpy_sections(checker, docstring, section_contexts), + Some(Convention::Google) => { + check_section_order(checker, section_contexts, google_section_order); + parse_google_sections(checker, docstring, section_contexts); + } + Some(Convention::Numpy) => { + check_section_order(checker, section_contexts, numpy_section_order); + parse_numpy_sections(checker, docstring, section_contexts); + } Some(Convention::Pep257) | None => match section_contexts.style() { - SectionStyle::Google => parse_google_sections(checker, docstring, section_contexts), - SectionStyle::Numpy => parse_numpy_sections(checker, docstring, section_contexts), + SectionStyle::Google => { + check_section_order(checker, section_contexts, google_section_order); + parse_google_sections(checker, docstring, section_contexts); + } + SectionStyle::Numpy => { + check_section_order(checker, section_contexts, numpy_section_order); + parse_numpy_sections(checker, docstring, section_contexts); + } }, } } @@ -2062,3 +2168,116 @@ fn parse_google_sections( } } } + +/// Canonical ordering of NumPy-style docstring sections. +/// +/// Variant order matches the [numpydoc style guide](https://numpydoc.readthedocs.io/en/latest/format.html). +#[derive(PartialEq, Eq, PartialOrd)] +enum NumpySectionOrder { + ShortSummary, + ExtendedSummary, + Parameters, + Returns, + Yields, + Receives, + OtherParameters, + Raises, + Warns, + Warnings, + SeeAlso, + Notes, + References, + Examples, + Attributes, + Methods, +} + +fn numpy_section_order(kind: SectionKind) -> Option { + match kind { + SectionKind::ShortSummary => Some(NumpySectionOrder::ShortSummary), + SectionKind::ExtendedSummary => Some(NumpySectionOrder::ExtendedSummary), + SectionKind::Parameters => Some(NumpySectionOrder::Parameters), + SectionKind::Returns => Some(NumpySectionOrder::Returns), + SectionKind::Yields => Some(NumpySectionOrder::Yields), + SectionKind::Receives => Some(NumpySectionOrder::Receives), + SectionKind::OtherParams | SectionKind::OtherParameters => { + Some(NumpySectionOrder::OtherParameters) + } + SectionKind::Raises => Some(NumpySectionOrder::Raises), + SectionKind::Warns => Some(NumpySectionOrder::Warns), + SectionKind::Warnings => Some(NumpySectionOrder::Warnings), + SectionKind::SeeAlso => Some(NumpySectionOrder::SeeAlso), + SectionKind::Notes => Some(NumpySectionOrder::Notes), + SectionKind::References => Some(NumpySectionOrder::References), + SectionKind::Examples => Some(NumpySectionOrder::Examples), + SectionKind::Attributes => Some(NumpySectionOrder::Attributes), + SectionKind::Methods => Some(NumpySectionOrder::Methods), + _ => None, + } +} + +/// Canonical ordering of Google-style docstring sections. +/// +/// Only enforces the ordering explicitly documented in the +/// [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods): +/// Args before Returns/Yields, Raises after those. All other sections are +/// unordered and return `None`. +#[derive(PartialEq, Eq, PartialOrd)] +enum GoogleSectionOrder { + Args, + Returns, + Yields, + Raises, +} + +fn google_section_order(kind: SectionKind) -> Option { + match kind { + SectionKind::Args + | SectionKind::Arguments + | SectionKind::KeywordArgs + | SectionKind::KeywordArguments + | SectionKind::OtherArgs + | SectionKind::OtherArguments => Some(GoogleSectionOrder::Args), + SectionKind::Returns | SectionKind::Return => Some(GoogleSectionOrder::Returns), + SectionKind::Yields | SectionKind::Yield => Some(GoogleSectionOrder::Yields), + SectionKind::Raises => Some(GoogleSectionOrder::Raises), + _ => None, + } +} + +/// D420 +fn check_section_order( + checker: &Checker, + section_contexts: &SectionContexts, + order_fn: fn(SectionKind) -> Option

, +) { + if !checker.is_rule_enabled(Rule::IncorrectSectionOrder) { + return; + } + + let mut max_order: Option<(P, &str)> = None; + + for context in section_contexts { + let Some(position) = order_fn(context.kind()) else { + continue; + }; + + if let Some((ref prev_pos, prev_name)) = max_order + && position < *prev_pos + { + checker.report_diagnostic( + IncorrectSectionOrder { + current: context.section_name().to_string(), + previous: prev_name.to_string(), + }, + context.section_name_range(), + ); + // Don't update max_order: keep tracking against the highest-seen + // position so that subsequent sections are compared against the + // same out-of-place section. + continue; + } + + max_order = Some((position, context.section_name())); + } +} diff --git a/crates/ruff_linter/src/rules/pydocstyle/settings.rs b/crates/ruff_linter/src/rules/pydocstyle/settings.rs index bd2b624da27a4..f2a7389b076f7 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/settings.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/settings.rs @@ -70,6 +70,7 @@ impl Convention { Rule::MissingTerminalPunctuation, Rule::MissingSectionNameColon, Rule::UndocumentedParam, + Rule::IncorrectSectionOrder, ], } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D420_D420_google.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D420_D420_google.py.snap new file mode 100644 index 0000000000000..63a42a43f2eec --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D420_D420_google.py.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- +D420 Section "Args" appears after section "Returns" but should be before it + --> D420_google.py:36:5 + | +34 | int +35 | +36 | Args: + | ^^^^ +37 | x: Description. +38 | """ + | + +D420 Section "Returns" appears after section "Raises" but should be before it + --> D420_google.py:47:5 + | +45 | ValueError +46 | +47 | Returns: + | ^^^^^^^ +48 | int +49 | """ + | + +D420 Section "Args" appears after section "Raises" but should be before it + --> D420_google.py:58:5 + | +56 | ValueError +57 | +58 | Args: + | ^^^^ +59 | x: Description. + | + +D420 Section "Returns" appears after section "Raises" but should be before it + --> D420_google.py:61:5 + | +59 | x: Description. +60 | +61 | Returns: + | ^^^^^^^ +62 | int +63 | """ + | + +D420 Section "Arguments" appears after section "Returns" but should be before it + --> D420_google.py:106:5 + | +104 | int +105 | +106 | Arguments: + | ^^^^^^^^^ +107 | x: Description. +108 | """ + | + +D420 Section "Args" appears after section "Returns" but should be before it + --> D420_google.py:155:5 + | +153 | Some notes. +154 | +155 | Args: + | ^^^^ +156 | x: Description. +157 | """ + | + +D420 Section "Args" appears after section "Returns" but should be before it + --> D420_google.py:189:5 + | +187 | int +188 | +189 | Args: + | ^^^^ +190 | x: Description. +191 | """ + | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D420_D420_numpy.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D420_D420_numpy.py.snap new file mode 100644 index 0000000000000..068e54100d50a --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D420_D420_numpy.py.snap @@ -0,0 +1,79 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- +D420 Section "Returns" appears after section "Notes" but should be before it + --> D420_numpy.py:78:5 + | +76 | Some notes. +77 | +78 | Returns + | ^^^^^^^ +79 | ------- +80 | int + | + +D420 Section "Returns" appears after section "Examples" but should be before it + --> D420_numpy.py:91:5 + | +89 | >>> func() +90 | +91 | Returns + | ^^^^^^^ +92 | ------- +93 | int + | + +D420 Section "Notes" appears after section "Examples" but should be before it + --> D420_numpy.py:95:5 + | +93 | int +94 | +95 | Notes + | ^^^^^ +96 | ----- +97 | Some notes. + | + +D420 Section "Warns" appears after section "Notes" but should be before it + --> D420_numpy.py:117:5 + | +115 | Some notes. +116 | +117 | Warns + | ^^^^^ +118 | ----- +119 | UserWarning + | + +D420 Section "Parameters" appears after section "Other Params" but should be before it + --> D420_numpy.py:131:5 + | +129 | Description. +130 | +131 | Parameters + | ^^^^^^^^^^ +132 | ---------- +133 | x : int + | + +D420 Section "Attributes" appears after section "Methods" but should be before it + --> D420_numpy.py:177:5 + | +175 | do_something +176 | +177 | Attributes + | ^^^^^^^^^^ +178 | ---------- +179 | x : int + | + +D420 Section "Returns" appears after section "Notes" but should be before it + --> D420_numpy.py:222:5 + | +220 | Some notes. +221 | +222 | Returns + | ^^^^^^^ +223 | ------- +224 | int + | diff --git a/ruff.schema.json b/ruff.schema.json index ebcd8c1f39c97..2ed8873a2f522 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3242,6 +3242,8 @@ "D417", "D418", "D419", + "D42", + "D420", "DJ", "DJ0", "DJ00", From 093a88f6df3f794c8efee6bf56ca42ca464e3dbd Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Wed, 25 Feb 2026 20:48:57 +0100 Subject: [PATCH 087/261] [ty] Fix infinite hang on mutually recursive TypeAliasType definitions (#23397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix an infinite hang when type-checking files with mutually recursive `TypeAliasType` definitions created via the manual constructor: ```python from typing import Union from typing_extensions import TypeAliasType A = TypeAliasType('A', Union[str, 'B']) B = TypeAliasType('B', list[A]) ``` This was discovered while testing ty on pydantic's [`tests/test_type_alias_type.py`](https://github.com/pydantic/pydantic/blob/main/tests/test_type_alias_type.py), which contains self-referential `TypeAliasType` definitions such as: ```python JsonType = TypeAliasType( "JsonType", Union[list["JsonType"], dict[str, "JsonType"], str, int, float, bool, None] ) ``` ## Root Cause `ManualPEP695TypeAliasType` was a `#[salsa::interned]` struct with the resolved value type as one of its fields: ```rust #[salsa::interned] pub struct ManualPEP695TypeAliasType<'db> { pub name: ast::name::Name, pub definition: Option>, pub value: Type<'db>, // <-- causes non-convergence } ``` With `#[salsa::interned]`, **all fields contribute to identity/deduplication**. When Salsa detects a cycle in `infer_definition_types`, it re-executes the function iteratively until the result converges (stops changing). But each iteration produces a different `value` type as the cycle resolves, creating a new interned ID each time: - **Iteration 1**: value = `Union[str, Divergent]` (B not yet resolved) → interned ID **X1** - **Iteration 2**: value = `Union[str, TypeAlias_B]` (B resolved) → interned ID **X2** - **Iteration 3**: yet another value → yet another interned ID **X3** - ... **never converges** This contrasts with `PEP695TypeAliasType` (the `type X = ...` form), which does NOT store the value in the interned struct and instead computes it lazily via a separate tracked method with its own cycle detection. ## Fix Remove the `value` field from `ManualPEP695TypeAliasType` and compute it lazily (matching the `PEP695TypeAliasType` pattern): 1. **`types.rs`**: Removed `value` field. Added a lazy `value_type()` tracked method with Salsa cycle annotations that re-parses the value from the definition's AST on demand. 2. **`infer/builder.rs`**: Created a dedicated `infer_typealiastype_call` method, following the same pattern used by `TypeVar`, `ParamSpec`, and `NewType`. This validates the arguments, constructs the `ManualPEP695TypeAliasType`, and defers inference of the value argument to avoid cycles. Also added an "invalid context" diagnostic when `TypeAliasType` is used outside a simple variable assignment. 3. **`class.rs`**: Removed the `TypeAliasType` handling from `KnownClass::check_call` entirely, since it is now fully handled by the dedicated inference method. Additional improvements: - Added validation that the first argument is a string literal matching the assignment target name (like `TypeVar` and `NewType`). - Corrected the error message for non-string-literal names (was referencing `typing.TypeAlias`, now says `TypeAliasType`). - The self-referential `JSONValue` test case in `cycle.md` now correctly resolves to the actual type instead of `Divergent`, since lazy evaluation means the alias is already bound by the time `value_type()` runs. - Goto-type-definition for `TypeAliasType` variables now correctly navigates to the assignment (previously returned "No type definitions found" because the definition was `None`). ## Approaches Explored (and rejected) ### `#[salsa::tracked]` with `#[no_eq]` on value Changed from `#[salsa::interned]` to `#[salsa::tracked]` with `#[no_eq]` on the value field. **Result**: Still hangs — tracked struct recycling does NOT work during Salsa cycle re-iterations. Each iteration creates a new tracked struct with a fresh ID (observed IDs growing unboundedly: 9800, 9801, 9802, ...). ### Register RHS as standalone expression in the semantic index builder Registered the call expression RHS as a standalone expression so that `check_call` could find the definition via `try_expression`. **Result**: Breaks `TypeVar`, `ParamSpec`, `NamedTuple`, and `NewType` handling, since `infer_assignment_definition_impl` has a code path for standalone expressions (generic inference) and a separate code path with specialized handlers for these known classes. Registering call expressions as standalone causes all of them to take the wrong path. ## Test plan - Added new mdtest for mutually recursive `TypeAliasType` definitions in `pep695_type_aliases.md` - Added mdtest for name-mismatch and invalid-context diagnostics - Updated `cycle.md` test expectation (improved from `Divergent` to actual resolved type) - Updated `ty_ide` snapshot for `goto_type_of_bare_type_alias_type` (now correctly finds the definition) - All 325 mdtests pass - All unit tests pass - All corpus tests pass --------- Co-authored-by: Carl Meyer --- crates/ty_ide/src/goto_type_definition.rs | 22 ++- .../resources/mdtest/cycle.md | 3 +- .../resources/mdtest/pep695_type_aliases.md | 58 +++++- crates/ty_python_semantic/src/types.rs | 101 +++++------ .../src/types/call/arguments.rs | 24 +-- crates/ty_python_semantic/src/types/class.rs | 46 +---- .../ty_python_semantic/src/types/display.rs | 15 +- .../src/types/infer/builder.rs | 169 +++++++++++++++--- 8 files changed, 278 insertions(+), 160 deletions(-) diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index 451517ef29663..f1688037a6fc4 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -717,8 +717,26 @@ mod tests { "#, ); - // TODO: This should jump to the definition of `Alias` above. - assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type definition]: Go to type definition + --> main.py:6:1 + | + 4 | Alias = TypeAliasType("Alias", tuple[int, int]) + 5 | + 6 | Alias + | ^^^^^ Clicking here + | + info: Found 1 type definition + --> main.py:4:1 + | + 2 | from typing_extensions import TypeAliasType + 3 | + 4 | Alias = TypeAliasType("Alias", tuple[int, int]) + | ----- + 5 | + 6 | Alias + | + "#); } #[test] diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 775b4e8ac6d58..8f5816fdc4953 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -54,8 +54,7 @@ JSONPrimitive = Union[str, int, float, bool, None] JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]') def _(x: JSONValue): - # TODO: should be `JSONValue` - reveal_type(x) # revealed: Divergent + reveal_type(x) # revealed: Sequence[JSONValue] | int | float | None | Mapping[str, JSONValue] ``` ## Self-referential legacy type variables diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 0b39f45bb44b3..ac6b77d51dac7 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -227,6 +227,23 @@ def f(x: IntAndT[str]) -> None: reveal_type(x) # revealed: Unknown ``` +### Generic value binds type variables to alias definition + +```py +from typing import Generic +from typing_extensions import TypeAliasType, TypeVar + +T = TypeVar("T", bound=int) +A = TypeAliasType("A", tuple[T], type_params=(T,)) + +S = TypeVar("S", bound=tuple[int]) + +class C(Generic[S]): + pass + +x: C[A] +``` + ### Error cases #### Name is not a string literal @@ -237,10 +254,49 @@ from typing_extensions import TypeAliasType def get_name() -> str: return "IntOrStr" -# error: [invalid-type-alias-type] "The name of a `typing.TypeAlias` must be a string literal" +# error: [invalid-type-alias-type] "The first argument to `TypeAliasType` must be a string literal" IntOrStr = TypeAliasType(get_name(), int | str) ``` +#### Name does not match variable + +```py +from typing_extensions import TypeAliasType + +# error: [invalid-type-alias-type] "The name of a `TypeAliasType` (`WrongName`) must match the name of the variable it is assigned to (`IntOrStr`)" +IntOrStr = TypeAliasType("WrongName", int | str) +``` + +#### Not a simple variable assignment + +`TypeAliasType` must be used in a simple variable assignment. Using it as a standalone expression or +in a tuple unpacking is not supported. + +```py +from typing_extensions import TypeAliasType + +# error: [invalid-type-alias-type] "A `TypeAliasType` definition must be a simple variable assignment" +TypeAliasType("IntOrStr", int | str) +``` + +### Mutually recursive `TypeAliasType` definitions + +Mutually recursive type aliases created via the `TypeAliasType` constructor should not cause the +type checker to hang. The value type is computed lazily to break cycles. + +```py +from typing_extensions import TypeAliasType, Union + +A = TypeAliasType("A", Union[str, "B"]) +B = TypeAliasType("B", list[A]) + +def f(x: A) -> None: + reveal_type(x) # revealed: str | list[A] + +def g(x: B) -> None: + reveal_type(x) # revealed: list[A] +``` + ## Cyclic aliases ### Self-referential diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7445afd06edb3..5190191c401df 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6912,7 +6912,7 @@ impl<'db> Type<'db> { Some(TypeDefinition::TypeVar(var.definition(db)?)) } KnownInstanceType::TypeAliasType(type_alias) => { - type_alias.definition(db).map(TypeDefinition::TypeAlias) + Some(TypeDefinition::TypeAlias(type_alias.definition(db))) } KnownInstanceType::NewType(newtype) => Some(TypeDefinition::NewType(newtype.definition(db))), _ => None, @@ -7549,9 +7549,7 @@ impl<'db> KnownInstanceType<'db> { Self::SubscriptedGeneric(context.normalized_impl(db, visitor)) } Self::TypeVar(typevar) => Self::TypeVar(typevar.normalized_impl(db, visitor)), - Self::TypeAliasType(type_alias) => { - Self::TypeAliasType(type_alias.normalized_impl(db, visitor)) - } + Self::TypeAliasType(type_alias) => Self::TypeAliasType(type_alias), Self::Field(field) => Self::Field(field.normalized_impl(db, visitor)), Self::UnionType(instance) => Self::UnionType(instance.normalized_impl(db, visitor)), Self::Literal(ty) => Self::Literal(ty.normalized_impl(db, visitor)), @@ -7589,9 +7587,7 @@ impl<'db> KnownInstanceType<'db> { Self::Deprecated(deprecated) => Some(Self::Deprecated(deprecated)), Self::ConstraintSet(set) => Some(Self::ConstraintSet(set)), Self::TypeVar(typevar) => Some(Self::TypeVar(typevar)), - Self::TypeAliasType(type_alias) => type_alias - .recursive_type_normalized_impl(db, div) - .map(Self::TypeAliasType), + Self::TypeAliasType(type_alias) => Some(Self::TypeAliasType(type_alias)), Self::Field(field) => field .recursive_type_normalized_impl(db, div, nested) .map(Self::Field), @@ -11955,14 +11951,13 @@ impl<'db> PEP695TypeAliasType<'db> { GenericContext::from_type_params(db, index, definition, type_params) }) } - - fn normalized_impl(self, _db: &'db dyn Db, _visitor: &NormalizedVisitor<'db>) -> Self { - self - } } /// A PEP 695 `types.TypeAliasType` created by manually calling the constructor. /// +/// The value type is computed lazily via [`ManualPEP695TypeAliasType::value_type()`] +/// to avoid cycle non-convergence for mutually recursive definitions. +/// /// # Ordering /// Ordering is based on the type alias's salsa-assigned id and not on its values. /// The id may change between runs, or when the alias was garbage collected and recreated. @@ -11971,8 +11966,7 @@ impl<'db> PEP695TypeAliasType<'db> { pub struct ManualPEP695TypeAliasType<'db> { #[returns(ref)] pub name: ast::name::Name, - pub definition: Option>, - pub value: Type<'db>, + pub definition: Definition<'db>, } // The Salsa heap is tracked separately. @@ -11983,28 +11977,38 @@ fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( type_alias: ManualPEP695TypeAliasType<'db>, visitor: &V, ) { - visitor.visit_type(db, type_alias.value(db)); + visitor.visit_type(db, type_alias.value_type(db)); } +#[salsa::tracked] impl<'db> ManualPEP695TypeAliasType<'db> { - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::new( - db, - self.name(db), - self.definition(db), - self.value(db).normalized_impl(db, visitor), - ) - } - - // TODO: with full support for manual PEP-695 style type aliases, this method should become unnecessary. - fn recursive_type_normalized_impl(self, db: &'db dyn Db, div: Type<'db>) -> Option { - Some(Self::new( - db, - self.name(db), - self.definition(db), - self.value(db) - .recursive_type_normalized_impl(db, div, true)?, - )) + /// The value type of this manual type alias. + /// + /// Computed lazily from the definition to avoid including the value in the interned + /// struct's identity. Returns `Divergent` if the type alias is defined cyclically. + #[salsa::tracked( + cycle_initial=|_, id, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { + value.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + let definition = self.definition(db); + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + return Type::unknown(); + }; + let value_node = assignment.value(&module); + let ast::Expr::Call(call) = value_node else { + return Type::unknown(); + }; + // The value is the second positional argument to TypeAliasType(name, value). + let Some(value_arg) = call.arguments.find_argument_value("value", 1) else { + return Type::unknown(); + }; + definition_expression_type(db, definition, value_arg) } } @@ -12037,26 +12041,6 @@ fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } impl<'db> TypeAliasType<'db> { - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - TypeAliasType::PEP695(type_alias) => { - TypeAliasType::PEP695(type_alias.normalized_impl(db, visitor)) - } - TypeAliasType::ManualPEP695(type_alias) => { - TypeAliasType::ManualPEP695(type_alias.normalized_impl(db, visitor)) - } - } - } - - fn recursive_type_normalized_impl(self, db: &'db dyn Db, div: Type<'db>) -> Option { - match self { - TypeAliasType::PEP695(type_alias) => Some(TypeAliasType::PEP695(type_alias)), - TypeAliasType::ManualPEP695(type_alias) => Some(TypeAliasType::ManualPEP695( - type_alias.recursive_type_normalized_impl(db, div)?, - )), - } - } - pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { match self { TypeAliasType::PEP695(type_alias) => type_alias.name(db), @@ -12064,9 +12048,9 @@ impl<'db> TypeAliasType<'db> { } } - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { match self { - TypeAliasType::PEP695(type_alias) => Some(type_alias.definition(db)), + TypeAliasType::PEP695(type_alias) => type_alias.definition(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.definition(db), } } @@ -12074,14 +12058,14 @@ impl<'db> TypeAliasType<'db> { pub fn value_type(self, db: &'db dyn Db) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.value_type(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.value(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), } } pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.raw_value_type(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.value(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), } } @@ -12153,10 +12137,7 @@ impl<'db> QualifiedTypeAliasName<'db> { /// For example, calling this method on a type alias `D` inside a class `C` in module `a.b` /// would return `["a", "b", "C"]`. pub(crate) fn components_excluding_self(&self) -> Vec { - let Some(definition) = self.type_alias.definition(self.db) else { - return vec![]; - }; - + let definition = self.type_alias.definition(self.db); let file = definition.file(self.db); let file_scope_id = definition.file_scope(self.db); diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 31f266bbf1d99..239c02a3bfe14 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -438,8 +438,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { mod tests { use crate::db::tests::setup_db; use crate::types::tuple::TupleType; - use crate::types::{KnownClass, ManualPEP695TypeAliasType, Type, TypeAliasType, UnionType}; - use ruff_python_ast as ast; + use crate::types::{KnownClass, Type, UnionType}; use super::expand_type; @@ -457,27 +456,6 @@ mod tests { assert_eq!(expanded, types); } - #[test] - fn expand_pep695_type_alias() { - let db = setup_db(); - let types = [ - KnownClass::Int.to_instance(&db), - KnownClass::Str.to_instance(&db), - KnownClass::Bytes.to_instance(&db), - ]; - let union_type = UnionType::from_elements(&db, types); - let alias_type = - Type::TypeAlias(TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( - &db, - ast::name::Name::new_static("MyAlias"), - None, - union_type, - ))); - let expanded = expand_type(&db, alias_type).unwrap(); - assert_eq!(expanded.len(), types.len()); - assert_eq!(expanded, types); - } - #[test] fn expand_bool_type() { let db = setup_db(); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 80dffc7745511..7dde5cca4de71 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -19,9 +19,7 @@ use crate::semantic_index::{ use crate::types::bound_super::BoundSuperError; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::context::InferContext; -use crate::types::diagnostic::{ - INVALID_DATACLASS_OVERRIDE, INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD, -}; +use crate::types::diagnostic::{INVALID_DATACLASS_OVERRIDE, SUPER_CALL_IN_NAMED_TUPLE_METHOD}; use crate::types::enums::{ enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value, }; @@ -46,9 +44,9 @@ use crate::types::{ ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType, CallableTypeKind, CallableTypes, DATACLASS_FLAGS, DataclassFlags, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownInstanceType, - ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType, - TypeAliasType, TypeContext, TypeMapping, TypedDictParams, UnionBuilder, VarianceInferable, - binding_type, declaration_type, determine_upper_bound, + MaterializationKind, NormalizedVisitor, PropertyInstanceType, TypeContext, TypeMapping, + TypedDictParams, UnionBuilder, VarianceInferable, binding_type, declaration_type, + determine_upper_bound, }; use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, @@ -8191,42 +8189,6 @@ impl KnownClass { ))); } - KnownClass::TypeAliasType => { - let assigned_to = index - .try_expression(ast::ExprRef::from(call_expression)) - .and_then(|expr| expr.assigned_to(db)); - - let containing_assignment = assigned_to.as_ref().and_then(|assigned_to| { - match assigned_to.node(module).targets.as_slice() { - [ast::Expr::Name(target)] => Some(index.expect_single_definition(target)), - _ => None, - } - }); - - let [Some(name), Some(value), ..] = overload.parameter_types() else { - return; - }; - - let Some(name) = name.as_string_literal() else { - if let Some(builder) = - context.report_lint(&INVALID_TYPE_ALIAS_TYPE, call_expression) - { - builder.into_diagnostic( - "The name of a `typing.TypeAlias` must be a string literal", - ); - } - return; - }; - overload.set_return_type(Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( - db, - ast::name::Name::new(name.value(db)), - containing_assignment, - value, - )), - ))); - } - _ => {} } } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 6428051d4bf5c..cab6d46f29d49 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -770,14 +770,13 @@ impl<'db> FmtDetailed<'db> for TypeAliasDisplay<'db> { } if qualification_level == Some(&QualificationLevel::FileAndLineNumber) { - if let Some(definition) = self.type_alias.definition(self.db) { - let file = definition.file(self.db); - let offset = definition - .focus_range(self.db, &parsed_module(self.db, file).load(self.db)) - .range() - .start(); - fmt_file_location(self.db, file, offset, f)?; - } + let definition = self.type_alias.definition(self.db); + let file = definition.file(self.db); + let offset = definition + .focus_range(self.db, &parsed_module(self.db, file).load(self.db)) + .range() + .start(); + fmt_file_location(self.db, file, offset, f)?; } Ok(()) } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index afc5c2eb33e6d..e7f2ff33d828b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -78,15 +78,15 @@ use crate::types::diagnostic::{ INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, INVALID_KEY, INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, - INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_BOUND, - INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, INVALID_TYPED_DICT_HEADER, - INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, - NOT_SUBSCRIPTABLE, PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE, - POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, - TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, - UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, - UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, + INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, + INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, + INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, + INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, MISSING_ARGUMENT, + NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, PARAMETER_ALREADY_ASSIGNED, + POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, + SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, + UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, + UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, @@ -131,14 +131,14 @@ use crate::types::{ BoundTypeVarIdentity, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypeKind, ClassType, DataclassParams, DynamicType, InternedConstraintSet, InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, - LintDiagnosticGuard, LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, - MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, - Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, - TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, TypeVarDefaultEvaluation, - TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, - UnionType, UnionTypeInstance, any_over_type, binding_type, definition_expression_type, - infer_complete_scope_types, infer_scope_types, todo_type, + LintDiagnosticGuard, LiteralValueType, LiteralValueTypeKind, ManualPEP695TypeAliasType, + MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, + ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, + Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, + TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, + TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, + TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, any_over_type, binding_type, + definition_expression_type, infer_complete_scope_types, infer_scope_types, todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; @@ -6762,6 +6762,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // signalling that we must fall back to normal call inference. self.infer_builtins_type_call(call_expr, Some(definition)) } + Some(KnownClass::TypeAliasType) => { + self.infer_typealiastype_call(target, call_expr, definition) + } Some(_) | None => { self.infer_call_expression_impl(call_expr, callable_type, tcx) } @@ -7320,6 +7323,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_newtype_assignment_deferred(arguments); return; } + (Some(KnownClass::TypeAliasType), InferenceRegion::Deferred(definition)) => { + self.infer_typealiastype_assignment_deferred(definition, arguments); + return; + } (Some(KnownClass::Type), InferenceRegion::Deferred(definition)) => { self.infer_builtins_type_deferred(definition, value); return; @@ -7433,6 +7440,113 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Infer a `TypeAliasType("Name", value)` call in a simple assignment context. + /// + /// Follows the same pattern as [`Self::infer_newtype_expression`]: validates the + /// arguments, constructs a [`ManualPEP695TypeAliasType`], and defers inference of + /// the value argument. + fn infer_typealiastype_call( + &mut self, + target: &ast::Expr, + call_expr: &ast::ExprCall, + definition: Definition<'db>, + ) -> Type<'db> { + fn error<'db>( + context: &InferContext<'db, '_>, + message: impl std::fmt::Display, + node: impl Ranged, + ) -> Type<'db> { + if let Some(builder) = context.report_lint(&INVALID_TYPE_ALIAS_TYPE, node) { + builder.into_diagnostic(message); + } + Type::unknown() + } + + let db = self.db(); + let arguments = &call_expr.arguments; + + if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { + return error( + &self.context, + "Starred arguments are not supported in `TypeAliasType` creation", + starred, + ); + } + + if arguments.args.len() != 2 { + return error( + &self.context, + format_args!( + "Wrong number of arguments in `TypeAliasType` creation: expected 2, found {}", + arguments.args.len() + ), + call_expr, + ); + } + + let name_param_ty = self.infer_expression(&arguments.args[0], TypeContext::default()); + + let Some(name) = name_param_ty.as_string_literal().map(|name| name.value(db)) else { + return error( + &self.context, + "The first argument to `TypeAliasType` must be a string literal", + &arguments.args[0], + ); + }; + + let ast::Expr::Name(ast::ExprName { + id: target_name, .. + }) = target + else { + return error( + &self.context, + "A `TypeAliasType` definition must be a simple variable assignment", + target, + ); + }; + + if name != target_name { + return error( + &self.context, + format_args!( + "The name of a `TypeAliasType` (`{name}`) must match \ + the name of the variable it is assigned to (`{target_name}`)" + ), + target, + ); + } + + // Inference of the value argument must be deferred, to avoid cycles. + self.deferred.insert(definition, self.multi_inference_state); + + Type::KnownInstance(KnownInstanceType::TypeAliasType( + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, + ast::name::Name::new(name), + definition, + )), + )) + } + + /// Infer the deferred value type of a `TypeAliasType`. + fn infer_typealiastype_assignment_deferred( + &mut self, + definition: Definition<'db>, + arguments: &ast::Arguments, + ) { + // Match the binding context used by eager assignment inference so legacy type variables + // in the alias value are bound to the alias definition. + let previous_context = self.typevar_binding_context.replace(definition); + + self.infer_type_expression(&arguments.args[1]); + // Infer keyword arguments (e.g. `type_params`) so their types are stored. + for keyword in &arguments.keywords { + self.infer_expression(&keyword.value, TypeContext::default()); + } + + self.typevar_binding_context = previous_context; + } + /// Deferred inference for assigned `type()` calls. /// /// Infers the bases argument that was skipped during initial inference to handle @@ -12519,11 +12633,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { report_attempted_protocol_instantiation(&self.context, call_expression, protocol); } - // Inference of correctly-placed `TypeVar`, `ParamSpec`, and `NewType` definitions - // is done in `infer_legacy_typevar`, `infer_paramspec`, and - // `infer_newtype_expression`, and doesn't use the full call-binding machinery. If - // we reach here, it means that someone is trying to instantiate one of these in an - // invalid context. + // Inference of correctly-placed `TypeVar`, `ParamSpec`, `NewType`, and + // `TypeAliasType` definitions is done in `infer_legacy_typevar`, + // `infer_paramspec`, `infer_newtype_expression`, and + // `infer_typealiastype_call`, and doesn't use the full call-binding + // machinery. If we reach here, it means that someone is trying to + // instantiate one of these in an invalid context. match class.known(self.db()) { Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar) => { if let Some(builder) = self @@ -12554,6 +12669,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } + Some(KnownClass::TypeAliasType) => { + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_ALIAS_TYPE, call_expression) + { + builder.into_diagnostic( + "A `TypeAliasType` definition must be a simple variable assignment", + ); + } + } _ => {} } } From f94b1c148b968c368eb3630e0e46a21776f43112 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 25 Feb 2026 20:06:52 +0000 Subject: [PATCH 088/261] Cache the union of two types as a tracked function (#23565) Co-authored-by: Claude --- crates/ty_python_semantic/src/place.rs | 15 +- .../reachability_constraints.rs | 2 +- crates/ty_python_semantic/src/types.rs | 135 ++++++++++-------- .../ty_python_semantic/src/types/call/bind.rs | 4 +- crates/ty_python_semantic/src/types/class.rs | 37 +++-- .../src/types/constraints.rs | 2 +- .../ty_python_semantic/src/types/function.rs | 8 +- .../ty_python_semantic/src/types/generics.rs | 4 +- .../src/types/infer/builder.rs | 36 ++--- crates/ty_python_semantic/src/types/narrow.rs | 6 +- 10 files changed, 134 insertions(+), 115 deletions(-) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 0885e9c899ffe..1d3c7ec66d96e 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -85,7 +85,7 @@ impl Widening { pub(crate) fn apply_if_needed<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { Self::None => ty, - Self::WithUnknown => UnionType::from_elements(db, [Type::unknown(), ty]), + Self::WithUnknown => UnionType::from_two_elements(db, Type::unknown(), ty), } } } @@ -326,13 +326,13 @@ impl<'db> LookupError<'db> { (LookupError::Undefined(_), _) => fallback, (LookupError::PossiblyUndefined { .. }, Err(LookupError::Undefined(_))) => Err(self), (LookupError::PossiblyUndefined(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new( - UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]), + UnionType::from_two_elements(db, ty.inner_type(), ty2.inner_type()), ty.origin().merge(ty2.origin()), ty.qualifiers().union(ty2.qualifiers()), )), (LookupError::PossiblyUndefined(ty), Err(LookupError::PossiblyUndefined(ty2))) => { Err(LookupError::PossiblyUndefined(TypeAndQualifiers::new( - UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]), + UnionType::from_two_elements(db, ty.inner_type(), ty2.inner_type()), ty.origin().merge(ty2.origin()), ty.qualifiers().union(ty2.qualifiers()), ))) @@ -920,7 +920,7 @@ pub(crate) fn place_by_id<'db>( definedness: boundness, .. }) => Place::Defined(DefinedPlace { - ty: UnionType::from_elements(db, [Type::unknown(), inferred]), + ty: UnionType::from_two_elements(db, Type::unknown(), inferred), origin, definedness: boundness, widening: Widening::None, @@ -979,7 +979,7 @@ pub(crate) fn place_by_id<'db>( definedness: boundness, .. }) => Place::Defined(DefinedPlace { - ty: UnionType::from_elements(db, [inferred_ty, declared_ty]), + ty: UnionType::from_two_elements(db, inferred_ty, declared_ty), origin, definedness: if boundness_analysis == BoundnessAnalysis::AssumeBound { Definedness::AlwaysDefined @@ -1968,9 +1968,10 @@ pub(crate) fn class_body_implicit_symbol<'db>( "__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(), "__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(), // __doc__ is `str` if there's a docstring, `None` if there isn't - "__doc__" => Place::bound(UnionType::from_elements( + "__doc__" => Place::bound(UnionType::from_two_elements( db, - [KnownClass::Str.to_instance(db), Type::none(db)], + KnownClass::Str.to_instance(db), + Type::none(db), )) .into(), // __firstlineno__ was added in Python 3.13 diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 27aab10a1358c..63be1e85d72e2 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -940,7 +940,7 @@ impl ReachabilityConstraints { false_accumulated, ); - UnionType::from_elements(db, [true_ty, false_ty]) + UnionType::from_two_elements(db, true_ty, false_ty) } } } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 5190191c401df..94de60a4352ea 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2902,7 +2902,7 @@ impl<'db> Type<'db> { if descr_get_boundness == Definedness::AlwaysDefined { bindings.return_type(db) } else { - UnionType::from_elements(db, [bindings.return_type(db), self]) + UnionType::from_two_elements(db, bindings.return_type(db), self) } }) // TODO: an error when calling `__get__` will lead to a `TypeError` or similar at runtime; @@ -3161,7 +3161,7 @@ impl<'db> Type<'db> { widening: fallback_widening, }), ) => Place::Defined(DefinedPlace { - ty: UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), + ty: UnionType::from_two_elements(db, meta_attr_ty, fallback_ty), origin: meta_origin.merge(fallback_origin), definedness: fallback_boundness, widening: fallback_widening, @@ -3205,7 +3205,7 @@ impl<'db> Type<'db> { widening: fallback_widening, }), ) => Place::Defined(DefinedPlace { - ty: UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), + ty: UnionType::from_two_elements(db, meta_attr_ty, fallback_ty), origin: meta_origin.merge(fallback_origin), definedness: meta_attr_boundness.max(fallback_boundness), widening: fallback_widening, @@ -4541,12 +4541,10 @@ impl<'db> Type<'db> { "object", )) // TODO: Should be `ReadableBuffer` instead of this union type: - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [ - KnownClass::Bytes.to_instance(db), - KnownClass::Bytearray.to_instance(db), - ], + KnownClass::Bytes.to_instance(db), + KnownClass::Bytearray.to_instance(db), )) .with_default_type(Type::bytes_literal(db, b"")), Parameter::positional_or_keyword(Name::new_static( @@ -4648,13 +4646,11 @@ impl<'db> Type<'db> { Parameter::positional_only(Some(Name::new_static("message"))) .with_annotated_type(Type::literal_string()), Parameter::keyword_only(Name::new_static("category")) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [ - // TODO: should be `type[Warning]` - Type::any(), - KnownClass::NoneType.to_instance(db), - ], + // TODO: should be `type[Warning]` + Type::any(), + KnownClass::NoneType.to_instance(db), )) // TODO: should be `type[Warning]` .with_default_type(Type::any()), @@ -4748,36 +4744,31 @@ impl<'db> Type<'db> { db, [ Parameter::positional_or_keyword(Name::new_static("fget")) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [ - Type::single_callable(db, getter_signature), - Type::none(db), - ], + Type::single_callable(db, getter_signature), + Type::none(db), )) .with_default_type(Type::none(db)), Parameter::positional_or_keyword(Name::new_static("fset")) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [ - Type::single_callable(db, setter_signature), - Type::none(db), - ], + Type::single_callable(db, setter_signature), + Type::none(db), )) .with_default_type(Type::none(db)), Parameter::positional_or_keyword(Name::new_static("fdel")) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [ - Type::single_callable(db, deleter_signature), - Type::none(db), - ], + Type::single_callable(db, deleter_signature), + Type::none(db), )) .with_default_type(Type::none(db)), Parameter::positional_or_keyword(Name::new_static("doc")) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [KnownClass::Str.to_instance(db), Type::none(db)], + KnownClass::Str.to_instance(db), + Type::none(db), )) .with_default_type(Type::none(db)), ], @@ -5657,9 +5648,10 @@ impl<'db> Type<'db> { // and the type returned by the `__getitem__` method. // // No diagnostic is emitted; iteration will always succeed! - Cow::Owned(TupleSpec::homogeneous(UnionType::from_elements( + Cow::Owned(TupleSpec::homogeneous(UnionType::from_two_elements( db, - [dunder_next_return, dunder_getitem_return_type], + dunder_next_return, + dunder_getitem_return_type, ))) }) .map_err(|dunder_getitem_error| { @@ -9974,9 +9966,10 @@ impl<'db> IterationError<'db> { } => match dunder_getitem_error { CallDunderError::MethodNotAvailable => Some(*dunder_next_return), CallDunderError::PossiblyUnbound(dunder_getitem_outcome) => { - Some(UnionType::from_elements( + Some(UnionType::from_two_elements( db, - [*dunder_next_return, dunder_getitem_outcome.return_type(db)], + *dunder_next_return, + dunder_getitem_outcome.return_type(db), )) } CallDunderError::CallError(CallErrorKind::NotCallable, _) => { @@ -9984,8 +9977,11 @@ impl<'db> IterationError<'db> { } CallDunderError::CallError(_, dunder_getitem_bindings) => { let dunder_getitem_return = dunder_getitem_bindings.return_type(db); - let elements = [*dunder_next_return, dunder_getitem_return]; - Some(UnionType::from_elements(db, elements)) + Some(UnionType::from_two_elements( + db, + *dunder_next_return, + dunder_getitem_return, + )) } }, @@ -11430,9 +11426,10 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("instance"))) .with_annotated_type(Type::object()), Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [KnownClass::Type.to_instance(db), Type::none(db)], + KnownClass::Type.to_instance(db), + Type::none(db), )) .with_default_type(Type::none(db)), ], @@ -11465,26 +11462,23 @@ impl<'db> KnownBoundMethodType<'db> { db, [ Parameter::positional_only(Some(Name::new_static("prefix"))) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [ - KnownClass::Str.to_instance(db), - Type::homogeneous_tuple( - db, - KnownClass::Str.to_instance(db), - ), - ], + KnownClass::Str.to_instance(db), + Type::homogeneous_tuple(db, KnownClass::Str.to_instance(db)), )), Parameter::positional_only(Some(Name::new_static("start"))) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], + KnownClass::SupportsIndex.to_instance(db), + Type::none(db), )) .with_default_type(Type::none(db)), Parameter::positional_only(Some(Name::new_static("end"))) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], + KnownClass::SupportsIndex.to_instance(db), + Type::none(db), )) .with_default_type(Type::none(db)), ], @@ -11555,9 +11549,10 @@ impl<'db> KnownBoundMethodType<'db> { db, [Parameter::keyword_only(Name::new_static("inferable")) .type_form() - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [Type::homogeneous_tuple(db, Type::any()), Type::none(db)], + Type::homogeneous_tuple(db, Type::any()), + Type::none(db), )) .with_default_type(Type::none(db))], ), @@ -11574,9 +11569,10 @@ impl<'db> KnownBoundMethodType<'db> { .with_annotated_type(KnownClass::ConstraintSet.to_instance(db)), ], ), - UnionType::from_elements( + UnionType::from_two_elements( db, - [KnownClass::Specialization.to_instance(db), Type::none(db)], + KnownClass::Specialization.to_instance(db), + Type::none(db), ), ))) } @@ -11635,9 +11631,10 @@ impl WrapperDescriptorKind { Parameter::positional_only(Some(Name::new_static("instance"))) .with_annotated_type(Type::object()), Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(UnionType::from_elements( + .with_annotated_type(UnionType::from_two_elements( db, - [type_instance, none], + type_instance, + none, )) .with_default_type(none), ], @@ -12187,9 +12184,13 @@ pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( // The Salsa heap is tracked separately. impl get_size2::GetSize for UnionType<'_> {} +#[salsa::tracked] impl<'db> UnionType<'db> { /// Create a union from a list of elements /// (which may be eagerly simplified into a different variant of [`Type`] altogether). + /// + /// For performance reasons, consider using [`UnionType::from_two_elements`] if + /// the union is constructed from exactly two elements. pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> where I: IntoIterator, @@ -12203,6 +12204,18 @@ impl<'db> UnionType<'db> { .build() } + /// Create a union type `A | B` from two elements `A` and `B`. + #[salsa::tracked( + cycle_initial=|_, id, _, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { + result.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + pub fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + UnionBuilder::new(db).add(a).add(b).build() + } + /// Create a union from a list of elements without unpacking type aliases. pub(crate) fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> where @@ -12552,12 +12565,10 @@ pub(crate) enum KnownUnion { impl KnownUnion { pub(crate) fn to_type(self, db: &dyn Db) -> Type<'_> { match self { - KnownUnion::Float => UnionType::from_elements( + KnownUnion::Float => UnionType::from_two_elements( db, - [ - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - ], + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), ), KnownUnion::Complex => UnionType::from_elements( db, diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 2be8bc8582054..e1806c2018267 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1415,7 +1415,7 @@ impl<'db> Bindings<'db> { }; let union_with_default = - |ty| UnionType::from_elements(db, [ty, default]); + |ty| UnionType::from_two_elements(db, ty, default); // TODO: we could emit a diagnostic here (if default is not set) overload.set_return_type( @@ -3802,7 +3802,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { if let Some(existing) = self.parameter_tys[parameter_index].replace(argument_type) { // We already verified in `match_parameters` that we only match multiple arguments // with variadic parameters. - let union = UnionType::from_elements(self.db, [existing, argument_type]); + let union = UnionType::from_two_elements(self.db, existing, argument_type); self.parameter_tys[parameter_index] = Some(union); } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 7dde5cca4de71..bedcc2845b9fa 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3223,7 +3223,7 @@ impl<'db> StaticClassLiteral<'db> { // def __gt__(self, other): return not (self == other or self < other) // If `__lt__` returns `int`, then `__gt__` could return `int | bool`. let return_ty = - UnionType::from_elements(db, [signature.return_ty, bool_ty]); + UnionType::from_two_elements(db, signature.return_ty, bool_ty); Signature::new_generic( signature.generic_context, signature.parameters().clone(), @@ -3500,7 +3500,11 @@ impl<'db> StaticClassLiteral<'db> { // This could probably be `weakref | None`, but it does not seem important enough to // model it precisely. - Some(UnionType::from_elements(db, [Type::any(), Type::none(db)])) + Some(UnionType::from_two_elements( + db, + Type::any(), + Type::none(db), + )) } (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { KnownClass::NamedTupleFallback @@ -3734,7 +3738,7 @@ impl<'db> StaticClassLiteral<'db> { if field.is_required() { field.declared_ty } else { - UnionType::from_elements(db, [field.declared_ty, Type::none(db)]) + UnionType::from_two_elements(db, field.declared_ty, Type::none(db)) }, ); @@ -3760,9 +3764,10 @@ impl<'db> StaticClassLiteral<'db> { if field.is_required() { field.declared_ty } else { - UnionType::from_elements( + UnionType::from_two_elements( db, - [field.declared_ty, Type::TypeVar(t_default)], + field.declared_ty, + Type::TypeVar(t_default), ) }, ); @@ -3781,7 +3786,7 @@ impl<'db> StaticClassLiteral<'db> { .with_annotated_type(KnownClass::Str.to_instance(db)), ], ), - UnionType::from_elements(db, [Type::unknown(), Type::none(db)]), + UnionType::from_two_elements(db, Type::unknown(), Type::none(db)), ) })) .chain(std::iter::once({ @@ -3804,9 +3809,10 @@ impl<'db> StaticClassLiteral<'db> { .with_annotated_type(Type::TypeVar(t_default)), ], ), - UnionType::from_elements( + UnionType::from_two_elements( db, - [Type::unknown(), Type::TypeVar(t_default)], + Type::unknown(), + Type::TypeVar(t_default), ), ) })); @@ -3864,9 +3870,10 @@ impl<'db> StaticClassLiteral<'db> { .with_annotated_type(Type::TypeVar(t_default)), ], ), - UnionType::from_elements( + UnionType::from_two_elements( db, - [field.declared_ty, Type::TypeVar(t_default)], + field.declared_ty, + Type::TypeVar(t_default), ), ); @@ -4702,9 +4709,10 @@ impl<'db> StaticClassLiteral<'db> { } else { Member { inner: Place::Defined(DefinedPlace { - ty: UnionType::from_elements( + ty: UnionType::from_two_elements( db, - [declared_ty, implicit_ty], + declared_ty, + implicit_ty, ), origin: TypeOrigin::Declared, definedness: declaredness, @@ -4764,9 +4772,10 @@ impl<'db> StaticClassLiteral<'db> { { Member { inner: Place::Defined(DefinedPlace { - ty: UnionType::from_elements( + ty: UnionType::from_two_elements( db, - [declared_ty, implicit_ty], + declared_ty, + implicit_ty, ), origin: TypeOrigin::Declared, definedness: declaredness, diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 162040260d10f..de0c341337333 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -787,7 +787,7 @@ impl<'db> ConstrainedTypeVar<'db> { } // (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂)) - let lower = UnionType::from_elements(db, [self.lower(db), other.lower(db)]); + let lower = UnionType::from_two_elements(db, self.lower(db), other.lower(db)); let upper = IntersectionType::from_two_elements(db, self_upper, other_upper); // If `lower ≰ upper`, then the intersection is empty, since there is no type that is both diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 6cf2eba08a5eb..20a7b574c5e8b 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1460,12 +1460,10 @@ pub(super) fn function_body_kind<'db>( } = raise && infer_type(exc).is_subtype_of( db, - UnionType::from_elements( + UnionType::from_two_elements( db, - [ - KnownClass::NotImplementedError.to_class_literal(db), - KnownClass::NotImplementedError.to_instance(db), - ], + KnownClass::NotImplementedError.to_class_literal(db), + KnownClass::NotImplementedError.to_instance(db), ), ) { diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index f280444440397..c5802e465faf9 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1373,7 +1373,7 @@ impl<'db> Specialization<'db> { .zip(other.types(db)) .map(|(self_type, other_type)| match (self_type, other_type) { (unknown, known) | (known, unknown) if unknown.is_unknown() => *known, - _ => UnionType::from_elements(db, [self_type, other_type]), + _ => UnionType::from_two_elements(db, *self_type, *other_type), }) .collect(); // TODO: Combine the tuple specs too @@ -1869,7 +1869,7 @@ impl<'db> SpecializationBuilder<'db> { return; } - *entry.get_mut() = UnionType::from_elements(self.db, [*entry.get(), ty]); + *entry.get_mut() = UnionType::from_two_elements(self.db, *entry.get(), ty); } Entry::Vacant(entry) => { entry.insert(ty); diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index e7f2ff33d828b..e4917269554d4 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -386,7 +386,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match self.cycle_recovery { Some(existing) => { self.cycle_recovery = - Some(UnionType::from_elements(self.db(), [existing, other])); + Some(UnionType::from_two_elements(self.db(), existing, other)); } None => { self.cycle_recovery = Some(other); @@ -3489,7 +3489,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { let ty = if let Some(default_expr) = default_expr { let default_ty = self.file_expression_type(default_expr); - UnionType::from_elements(self.db(), [Type::unknown(), default_ty]) + UnionType::from_two_elements(self.db(), Type::unknown(), default_ty) } else if let Some(ty) = self.special_first_method_parameter_type(parameter) { ty } else { @@ -4265,12 +4265,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db())) } else if node_ty.is_assignable_to( self.db(), - UnionType::from_elements( + UnionType::from_two_elements( self.db(), - [ - type_base_exception, - Type::homogeneous_tuple(self.db(), type_base_exception), - ], + type_base_exception, + Type::homogeneous_tuple(self.db(), type_base_exception), ), ) { KnownClass::BaseException.to_instance(self.db()) @@ -8299,7 +8297,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Emit diagnostic for invalid types (not Iterable[Any] | None). let iterable_any = KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); - let valid_type = UnionType::from_elements(db, [iterable_any, Type::none(db)]); + let valid_type = UnionType::from_two_elements(db, iterable_any, Type::none(db)); if !kw_type.is_assignable_to(db, valid_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) @@ -8332,9 +8330,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } "module" if kind.is_collections() => { // Emit diagnostic for invalid types (not str | None). - let valid_type = UnionType::from_elements( + let valid_type = UnionType::from_two_elements( db, - [KnownClass::Str.to_instance(db), Type::none(db)], + KnownClass::Str.to_instance(db), + Type::none(db), ); if !kw_type.is_assignable_to(db, valid_type) && let Some(builder) = @@ -8520,7 +8519,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Emit diagnostic if the type is outright invalid (not str | Iterable[str]). let iterable_str = KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); let valid_type = - UnionType::from_elements(db, [KnownClass::Str.to_instance(db), iterable_str]); + UnionType::from_two_elements(db, KnownClass::Str.to_instance(db), iterable_str); if !fields_type.is_assignable_to(db, valid_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, fields_arg) { @@ -8699,7 +8698,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { SequenceKind::Tuple => Type::heterogeneous_tuple(db, [name_type, declared_type]), SequenceKind::List => KnownClass::List.to_specialized_instance( db, - &[UnionType::from_elements(db, [name_type, declared_type])], + &[UnionType::from_two_elements(db, name_type, declared_type)], ), }; @@ -9434,9 +9433,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let value_ty = infer_value_ty(self, TypeContext::default()); binary_return_ty(self, value_ty) } - Err(CallDunderError::PossiblyUnbound(outcome)) => UnionType::from_elements( + Err(CallDunderError::PossiblyUnbound(outcome)) => UnionType::from_two_elements( db, - [outcome.return_type(db), binary_return_ty(self, *value_ty)], + outcome.return_type(db), + binary_return_ty(self, *value_ty), ), Err(CallDunderError::CallError(_, bindings)) => { report_unsupported_augmented_assignment( @@ -9862,9 +9862,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let base_exception_instance = KnownClass::BaseException.to_instance(self.db()); let can_be_raised = - UnionType::from_elements(self.db(), [base_exception_type, base_exception_instance]); + UnionType::from_two_elements(self.db(), base_exception_type, base_exception_instance); let can_be_exception_cause = - UnionType::from_elements(self.db(), [can_be_raised, Type::none(self.db())]); + UnionType::from_two_elements(self.db(), can_be_raised, Type::none(self.db())); if let Some(raised) = exc { let raised_type = self.infer_expression(raised, TypeContext::default()); @@ -12244,7 +12244,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) { Truthiness::AlwaysTrue => body_ty, Truthiness::AlwaysFalse => orelse_ty, - Truthiness::Ambiguous => UnionType::from_elements(self.db(), [body_ty, orelse_ty]), + Truthiness::Ambiguous => UnionType::from_two_elements(self.db(), body_ty, orelse_ty), } } @@ -16131,7 +16131,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { UnionTypeInstance::new( db, None, - Ok(UnionType::from_elements(db, [ty, Type::none(db)])), + Ok(UnionType::from_two_elements(db, ty, Type::none(db))), ), )); } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index e892e6f033166..0aca7c4299bcd 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -442,7 +442,7 @@ fn merge_constraints_or<'db>( into_constraint.intersection_disjunct, from_constraint.intersection_disjunct, ) { - (Some(a), Some(b)) => Some(UnionType::from_elements(db, [a, b])), + (Some(a), Some(b)) => Some(UnionType::from_two_elements(db, a, b)), (Some(a), None) => Some(a), (None, Some(b)) => Some(b), (None, None) => None, @@ -864,7 +864,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } } (_, Some(LiteralValueTypeKind::Bool(b))) => Some( - UnionType::from_elements(self.db, [rhs_ty, Type::int_literal(i64::from(b))]) + UnionType::from_two_elements(self.db, rhs_ty, Type::int_literal(i64::from(b))) .negate(self.db), ), _ if rhs_ty.is_single_valued(self.db) => Some(rhs_ty.negate(self.db)), @@ -963,7 +963,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { .build(); // Keep order: first literal complement, then broader arms. - let result = UnionType::from_elements(self.db, [narrowed_single, rest_union]); + let result = UnionType::from_two_elements(self.db, narrowed_single, rest_union); Some(result) } else { None From 9a4be59b29297a42ca657e32d7a07154dbbaf94b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 25 Feb 2026 20:11:27 +0000 Subject: [PATCH 089/261] Temporarily remove AlexWaygood from reviewbot config (#23567) --- .github/pr-assignee-pools.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/pr-assignee-pools.toml b/.github/pr-assignee-pools.toml index 0fb0ce0c547a6..994a250e83ef9 100644 --- a/.github/pr-assignee-pools.toml +++ b/.github/pr-assignee-pools.toml @@ -1,12 +1,12 @@ [[pools]] name = "ty-semantic" paths = ["/crates/ty_python_semantic/**"] -reviewers = ["carljm", "AlexWaygood", "sharkdp", "dcreager", "ibraheemdev", "oconnor663"] +reviewers = ["carljm", "sharkdp", "dcreager", "ibraheemdev", "oconnor663"] [[pools]] name = "ty-module-resolver" paths = ["/crates/ty_module_resolver/**", "/crates/ty_site_packages/**"] -reviewers = ["carljm", "AlexWaygood", "MichaReiser", "BurntSushi"] +reviewers = ["carljm", "MichaReiser", "BurntSushi"] [[pools]] name = "ty-infra" @@ -28,7 +28,7 @@ reviewers = ["MichaReiser", "BurntSushi"] [[pools]] name = "ty-ide" paths = ["/crates/ty_ide/**"] -reviewers = ["MichaReiser", "AlexWaygood", "BurntSushi", "dhruvmanila"] +reviewers = ["MichaReiser", "BurntSushi", "dhruvmanila"] [[pools]] name = "ty-server" From 728609a06e590e8b458714b4e17d6b16828c21e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E5=81=9A=E4=BA=86=E7=9D=A1=E5=A4=A7=E8=A7=89?= <64798754+stakeswky@users.noreply.github.com> Date: Thu, 26 Feb 2026 05:27:48 +0800 Subject: [PATCH 090/261] [`ruff`] Suppress diagnostic for invalid f-strings before Python 3.12 (`RUF027`) (#23480) ## Summary Fixes #23460. Comments inside f-string interpolations (`#`) are only valid in Python 3.12+ ([PEP 701](https://peps.python.org/pep-0701/)). RUF027 was not checking for this, so it could produce invalid f-strings when the interpolation text contained a `#` and `target-version` was below 3.12. ## Fix Extended the existing `target_version < PY312` backslash check to also cover comments: ```rust if target_version < PythonVersion::PY312 && (interpolation_text.contains('\\') || interpolation_text.contains('#')) { return false; } ``` ## Test Plan Added a test fixture with a comment inside an f-string interpolation (`{x # }`). The existing `assert_diagnostics_diff!` test for PY311 vs PY312 will capture the new suppression. ## Reproducer (from #23460) ```python x = "!" print("""{x # } }""") ``` On `--target-version py311`, this should not trigger RUF027 since the resulting f-string would contain a comment in the interpolation, which is a syntax error before Python 3.12. --------- Co-authored-by: User Co-authored-by: stakeswky --- .../resources/test/fixtures/ruff/RUF027_0.py | 28 ++++++ .../ruff/rules/missing_fstring_syntax.rs | 21 +++-- ...ules__ruff__tests__RUF027_RUF027_0.py.snap | 89 +++++++++++++++++++ ...issing_fstring_syntax_backslash_py311.snap | 54 ++++++++++- 4 files changed, 183 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py index 04c482bd5226e..c984bfcef7983 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py @@ -90,3 +90,31 @@ def fuzz_bug(): def backslash_test(): x = "test" print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 + +# Test case for comment handling in f-string interpolations +# Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +def comment_test(): + x = "!" + print("""{x # } +}""") + +# Test case for `#` inside a nested string literal in interpolation +# `#` inside a string is NOT a comment — should trigger RUF027 even on Python < 3.12 +def hash_in_string_test(): + x = "world" + print("Hello {'#'}{x}") # RUF027: `#` is inside a string, not a comment + print("Hello {\"#\"}{x}") # RUF027: same, double-quoted + +# Test case for `#` in format spec (e.g., `{1:#x}`) +# `#` in a format spec is NOT a comment — should trigger RUF027 even on Python < 3.12 +def hash_in_format_spec_test(): + n = 255 + print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment + print("Oct: {n:#o}") # RUF027: same + +# Test case for `#` in nested interpolation inside format spec (e.g., `{1:{x #}}`) +# The `#` is a comment inside a nested interpolation — should NOT trigger RUF027 on Python < 3.12 +def hash_in_nested_format_spec_test(): + x = 5 + print("""{1:{x #}} +}""") # Should not trigger RUF027 for Python < 3.12 diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index df64e8532bc85..5d4e9cb6e3c2b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -4,7 +4,7 @@ use rustc_hash::FxHashSet; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, PythonVersion}; use ruff_python_literal::format::FormatSpec; -use ruff_python_parser::parse_expression; +use ruff_python_parser::{UnsupportedSyntaxErrorKind, parse_expression}; use ruff_python_semantic::analyze::logging::is_logger_candidate; use ruff_python_semantic::{Modules, SemanticModel, TypingOnlyBindingsStatus}; use ruff_text_size::{Ranged, TextRange}; @@ -200,6 +200,18 @@ fn should_be_fstring( return false; }; + // For Python < 3.12, reject if the parser detected any PEP 701 f-string + // features. + if target_version < PythonVersion::PY312 { + let has_pep701 = parsed + .unsupported_syntax_errors() + .iter() + .any(|e| matches!(e.kind, UnsupportedSyntaxErrorKind::Pep701FString(_))); + if has_pep701 { + return false; + } + } + // Note: Range offsets for `value` are based on `fstring_expr` let ast::Expr::FString(ast::ExprFString { value, .. }) = parsed.expr() else { return false; @@ -226,13 +238,6 @@ fn should_be_fstring( for f_string in value.f_strings() { let mut has_name = false; for element in f_string.elements.interpolations() { - // Check if the interpolation expression contains backslashes - // F-strings with backslashes in interpolations are only valid in Python 3.12+ - let interpolation_text = &fstring_expr[element.range()]; - if target_version < PythonVersion::PY312 && interpolation_text.contains('\\') { - return false; - } - if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() { if arg_names.contains(id) { return false; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap index 977628639da71..30ec476ed013f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap @@ -328,6 +328,8 @@ RUF027 [*] Possible f-string without an `f` prefix 91 | x = "test" 92 | print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 | ^^^^^^^^^^^^^^^^^^ +93 | +94 | # Test case for comment handling in f-string interpolations | help: Add `f` prefix 89 | # Should not trigger RUF027 for Python < 3.12 due to backslashes in interpolations @@ -335,4 +337,91 @@ help: Add `f` prefix 91 | x = "test" - print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 92 + print(f"Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 +93 | +94 | # Test case for comment handling in f-string interpolations +95 | # Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:98:11 + | + 96 | def comment_test(): + 97 | x = "!" + 98 | print("""{x # } + | ___________^ + 99 | | }""") + | |____^ +100 | +101 | # Test case for `#` inside a nested string literal in interpolation + | +help: Add `f` prefix +95 | # Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +96 | def comment_test(): +97 | x = "!" + - print("""{x # } +98 + print(f"""{x # } +99 | }""") +100 | +101 | # Test case for `#` inside a nested string literal in interpolation +note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:105:11 + | +103 | def hash_in_string_test(): +104 | x = "world" +105 | print("Hello {'#'}{x}") # RUF027: `#` is inside a string, not a comment + | ^^^^^^^^^^^^^^^^ +106 | print("Hello {\"#\"}{x}") # RUF027: same, double-quoted + | +help: Add `f` prefix +102 | # `#` inside a string is NOT a comment — should trigger RUF027 even on Python < 3.12 +103 | def hash_in_string_test(): +104 | x = "world" + - print("Hello {'#'}{x}") # RUF027: `#` is inside a string, not a comment +105 + print(f"Hello {'#'}{x}") # RUF027: `#` is inside a string, not a comment +106 | print("Hello {\"#\"}{x}") # RUF027: same, double-quoted +107 | +108 | # Test case for `#` in format spec (e.g., `{1:#x}`) +note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:112:11 + | +110 | def hash_in_format_spec_test(): +111 | n = 255 +112 | print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment + | ^^^^^^^^^^^^^ +113 | print("Oct: {n:#o}") # RUF027: same + | +help: Add `f` prefix +109 | # `#` in a format spec is NOT a comment — should trigger RUF027 even on Python < 3.12 +110 | def hash_in_format_spec_test(): +111 | n = 255 + - print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment +112 + print(f"Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment +113 | print("Oct: {n:#o}") # RUF027: same +114 | +115 | # Test case for `#` in nested interpolation inside format spec (e.g., `{1:{x #}}`) +note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:113:11 + | +111 | n = 255 +112 | print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment +113 | print("Oct: {n:#o}") # RUF027: same + | ^^^^^^^^^^^^^ +114 | +115 | # Test case for `#` in nested interpolation inside format spec (e.g., `{1:{x #}}`) + | +help: Add `f` prefix +110 | def hash_in_format_spec_test(): +111 | n = 255 +112 | print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment + - print("Oct: {n:#o}") # RUF027: same +113 + print(f"Oct: {n:#o}") # RUF027: same +114 | +115 | # Test case for `#` in nested interpolation inside format spec (e.g., `{1:{x #}}`) +116 | # The `#` is a comment inside a nested interpolation — should NOT trigger RUF027 on Python < 3.12 note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap index 8e6692f7a5628..beb416fa399a2 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap @@ -6,10 +6,33 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs +linter.unresolved_target_version = 3.11 --- Summary --- -Removed: 2 +Removed: 4 Added: 0 --- Removed --- +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:41:22 + | +39 | single_line = """ {a} """ # RUF027 +40 | # RUF027 +41 | multi_line = a = """b { # comment + | ______________________^ +42 | | c} d +43 | | """ + | |_______^ + | +help: Add `f` prefix +38 | c = a +39 | single_line = """ {a} """ # RUF027 +40 | # RUF027 + - multi_line = a = """b { # comment +41 + multi_line = a = f"""b { # comment +42 | c} d +43 | """ +44 | +note: This is an unsafe fix and may change runtime behavior + + RUF027 [*] Possible f-string without an `f` prefix --> RUF027_0.py:49:9 | @@ -40,6 +63,8 @@ RUF027 [*] Possible f-string without an `f` prefix 91 | x = "test" 92 | print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 | ^^^^^^^^^^^^^^^^^^ +93 | +94 | # Test case for comment handling in f-string interpolations | help: Add `f` prefix 89 | # Should not trigger RUF027 for Python < 3.12 due to backslashes in interpolations @@ -47,4 +72,31 @@ help: Add `f` prefix 91 | x = "test" - print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 92 + print(f"Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 +93 | +94 | # Test case for comment handling in f-string interpolations +95 | # Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +note: This is an unsafe fix and may change runtime behavior + + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:98:11 + | + 96 | def comment_test(): + 97 | x = "!" + 98 | print("""{x # } + | ___________^ + 99 | | }""") + | |____^ +100 | +101 | # Test case for `#` inside a nested string literal in interpolation + | +help: Add `f` prefix +95 | # Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +96 | def comment_test(): +97 | x = "!" + - print("""{x # } +98 + print(f"""{x # } +99 | }""") +100 | +101 | # Test case for `#` inside a nested string literal in interpolation note: This is an unsafe fix and may change runtime behavior From 09de8efcdf35f1a9c81e69af9e5c8892d2db90c9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 25 Feb 2026 23:05:55 +0000 Subject: [PATCH 091/261] [ty] Reimplement equivalence as mutual redundancy (#23428) Co-authored-by: Carl Meyer --- .../resources/mdtest/annotations/callable.md | 5 +- .../resources/mdtest/call/overloads.md | 76 +++ .../resources/mdtest/protocols.md | 71 +- .../type_properties/is_equivalent_to.md | 19 + .../mdtest/type_properties/materialization.md | 28 +- .../resources/mdtest/typed_dict.md | 4 +- crates/ty_python_semantic/src/types.rs | 621 +----------------- .../src/types/bound_super.rs | 144 +++- .../ty_python_semantic/src/types/builder.rs | 31 +- .../ty_python_semantic/src/types/call/bind.rs | 19 +- crates/ty_python_semantic/src/types/class.rs | 71 +- .../src/types/class_base.rs | 12 +- .../src/types/constraints.rs | 13 +- .../ty_python_semantic/src/types/display.rs | 62 +- .../ty_python_semantic/src/types/function.rs | 49 +- .../ty_python_semantic/src/types/generics.rs | 100 +-- .../ty_python_semantic/src/types/instance.rs | 137 +--- .../ty_python_semantic/src/types/literal.rs | 41 +- .../src/types/protocol_class.rs | 53 +- .../ty_python_semantic/src/types/relation.rs | 209 +++--- .../src/types/signatures.rs | 286 +------- .../src/types/subclass_of.rs | 20 +- crates/ty_python_semantic/src/types/tuple.rs | 128 +--- .../src/types/type_ordering.rs | 357 ---------- .../src/types/typed_dict.rs | 73 +- 25 files changed, 429 insertions(+), 2200 deletions(-) delete mode 100644 crates/ty_python_semantic/src/types/type_ordering.rs diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index ad566f868f4ab..1fd1fcd24a2a5 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -20,10 +20,13 @@ is the return type. Here, we explore various invalid forms. A bare `Callable` without any type arguments: ```py -from typing import Callable +from typing import Callable, Any +from ty_extensions import is_equivalent_to, static_assert def _(c: Callable): reveal_type(c) # revealed: (...) -> Unknown + +static_assert(is_equivalent_to(Callable, Callable[..., Any])) ``` ### Invalid parameter type argument diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 9b4d6bfb631be..cc58bc9b0ea55 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -1158,6 +1158,43 @@ def _(list_int: list[int], list_any: list[Any]): reveal_type(f(*(list_any,))) # revealed: int ``` +### Single list argument (complex generics) + +Here `Callable[P, R]` in the first overload is an equivalent type to `Callable[P, R]` in the second +overload, despite the fact that `P` and `R` are scoped to different overload-literals. Evaluation of +the result of the call is therefore unambiguous: it must evaluate to a `Callable` type rather +`Unknown`/`Any`: + +```toml +[environment] +python-version = "3.12" +``` + +`overloaded.pyi`: + +```pyi +from typing import overload, Callable + +class Foo: ... + +class Decorator: + @overload + def __call__[**P, R](self, task_runner: None) -> Callable[[Callable[P, R]], Callable[P, R]]: ... + @overload + def __call__[**P, R](self, task_runner: Foo) -> Callable[[Callable[P, R]], Callable[P, R]]: ... +``` + +`main.py`: + +```py +from typing import Any +from overloaded import Decorator + +def test(decorator: Decorator, argument: Any): + # revealed: [**P'return, R'return]((**P'return) -> R'return, /) -> ((**P'return) -> R'return) + reveal_type(decorator(argument)) +``` + ### Single list argument (ambiguous) The overload definition is the same as above, but the return type of the second overload is changed @@ -1786,6 +1823,45 @@ def _(arg: tuple[A | B, Any]): reveal_type(f(*(arg,))) # revealed: Unknown ``` +### Unknown argument with TypeVar overload + +When an `Unknown` argument is passed to an overloaded function where one overload has a concrete +parameter type and another uses a TypeVar, the parameter types must not be considered equivalent +during step 5's "participating parameter" determination. If TypeVar solving were allowed during that +equivalence check, the solver could find an assignment (e.g. `T = None`) that makes the parameters +look equivalent, causing step 5 to skip filtering entirely and incorrectly pick the first overload. + +`overloaded.pyi`: + +```pyi +from typing import TypeVar, overload, Literal, Iterable + +T = TypeVar("T") + +@overload +def f(components: Iterable[None]) -> Literal[b""]: ... +@overload +def f(components: Iterable[T | None]) -> T: ... +``` + +```py +from overloaded import f +from nonexistent_module import something_unknown # error: [unresolved-import] + +reveal_type(something_unknown) # revealed: Unknown + +# The result should be `Unknown`, not `Literal[b""]`. +reveal_type(f(something_unknown)) # revealed: Unknown +reveal_type(f((something_unknown, something_unknown, something_unknown))) # revealed: Unknown +reveal_type(f((something_unknown, None, something_unknown))) # revealed: Unknown + +# Concrete arguments should still resolve correctly. +def _(s: str): + reveal_type(f((s, s, None))) # revealed: str + +reveal_type(f((None, None, None))) # revealed: Literal[b""] +``` + ## Bidirectional Type Inference ```toml diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 8496b7bdb27f8..cdc5bfd1da28c 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -1726,8 +1726,7 @@ from ty_extensions import is_equivalent_to class HasMutableXAttr(Protocol): x: int -# TODO: should pass -static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error] +static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) @@ -2089,16 +2088,14 @@ S = TypeVar("S") class LegacyClassScoped(Protocol[S]): def method(self, input: S) -> None: ... -# TODO: these should pass -static_assert(is_equivalent_to(NewStyleClassScoped, LegacyClassScoped)) # error: [static-assert-error] -static_assert(is_equivalent_to(NewStyleClassScoped[int], LegacyClassScoped[int])) # error: [static-assert-error] +static_assert(is_equivalent_to(NewStyleClassScoped, LegacyClassScoped)) +static_assert(is_equivalent_to(NewStyleClassScoped[int], LegacyClassScoped[int])) class NominalGeneric[T]: def method(self, input: T) -> None: ... def _[T](x: T) -> T: - # TODO: should pass - static_assert(is_equivalent_to(NewStyleClassScoped[T], LegacyClassScoped[T])) # error: [static-assert-error] + static_assert(is_equivalent_to(NewStyleClassScoped[T], LegacyClassScoped[T])) static_assert(is_subtype_of(NominalGeneric[T], NewStyleClassScoped[T])) static_assert(is_subtype_of(NominalGeneric[T], LegacyClassScoped[T])) return x @@ -2176,9 +2173,7 @@ class NominalReturningOtherClass: def g(self) -> Other: raise NotImplementedError -# TODO: should pass -static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error] - +static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) static_assert(is_assignable_to(NominalNewStyle, NewStyleFunctionScoped)) static_assert(is_assignable_to(NominalNewStyle, LegacyFunctionScoped)) static_assert(is_subtype_of(NominalNewStyle, NewStyleFunctionScoped)) @@ -2386,9 +2381,7 @@ class P4(Protocol): def z(self, value: int) -> None: ... static_assert(is_equivalent_to(P1, P2)) - -# TODO: should pass -static_assert(is_equivalent_to(P3, P4)) # error: [static-assert-error] +static_assert(is_equivalent_to(P3, P4)) ``` As with protocols that only have non-method members, this also holds true when they appear in @@ -2399,9 +2392,7 @@ class A: ... class B: ... static_assert(is_equivalent_to(A | B | P1, P2 | B | A)) - -# TODO: should pass -static_assert(is_equivalent_to(A | B | P3, P4 | B | A)) # error: [static-assert-error] +static_assert(is_equivalent_to(A | B | P3, P4 | B | A)) ``` ## Subtyping between two protocol types with method members @@ -2948,8 +2939,6 @@ class Bar(Protocol): @property def x(self) -> "Bar": ... -# TODO: this should pass -# error: [static-assert-error] static_assert(is_equivalent_to(Foo, Bar)) T = TypeVar("T", bound="TypeVarRecursive") @@ -3429,12 +3418,12 @@ type of `float` while the other has `str`, we should decide that they're not ass However, zooming in to the implementation details, `_ReturnT_co` is actually the type of the `value` attribute on the `StopIteration` exception that the `Generator` raises when it's finished. This is -awkward, because protocols don't describe the exceptions that their methods raise. How is `ty` +awkward, because protocols don't describe the exceptions that their methods raise. How is ty supposed to see that incompatible `_ReturnT_co` types imply incompatible `Generator`s? As of Python 3.13, the `Generator` protocol's `close` method was changed from returning `None` to returning `_ReturnT_co | None`. This was motivated by an edge case (you tried to cancel a generator, -but it caught the related exception and returned something anyway), but coincidentally it tells `ty` +but it caught the related exception and returned something anyway), but coincidentally it tells ty what it needs to know: `_ReturnT_co` is something that some method in this protocol returns. Something with a method that returns `float` isn't assignable to something where the same method returns `str`. Problem solved. @@ -3450,7 +3439,7 @@ protocol (prior to 3.13) genuinely tells us nothing about how `_ReturnT_co` inte assignability. As a special case workaround for this, we compare `Generator` implementations *nominally* when the -target Python version is 3.12 or earlier, in both `has_relation_to` and `is_equivalent_to`. +target Python version is 3.12 or earlier in `has_relation_to`. ```toml [environment] @@ -3458,8 +3447,10 @@ python-version = "3.12" ``` ```py -from ty_extensions import is_equivalent_to, is_subtype_of, static_assert -from typing import Generator +from ty_extensions import is_equivalent_to, is_subtype_of, static_assert, is_assignable_to +from typing import Generator, Awaitable, Protocol, TypeVar, Any, Protocol + +T_co = TypeVar("T_co", covariant=True) class A: ... class B: ... @@ -3471,6 +3462,21 @@ static_assert(not is_subtype_of(Generator[None, None, B], Generator[None, None, static_assert(is_equivalent_to(Generator[None, None, A], Generator[None, None, A])) static_assert(is_subtype_of(Generator[None, None, A], Generator[None, None, A])) static_assert(is_subtype_of(Generator[None, None, A], Generator[None, None, A])) + +# Awaitable is also impacted, since `Awaitable.__await__` returns `Generator` + +static_assert(not is_equivalent_to(Awaitable[A], Awaitable[B])) +static_assert(not is_equivalent_to(Awaitable[A], Awaitable[Any])) +static_assert(not is_subtype_of(Awaitable[A], Awaitable[B])) +static_assert(not is_assignable_to(Awaitable[A], Awaitable[B])) + +class CustomCovariantProtocol(Protocol[T_co]): + def foo(self) -> tuple[list[Generator[None, None, T_co]]]: ... + +static_assert(not is_equivalent_to(CustomCovariantProtocol[A], CustomCovariantProtocol[B])) +static_assert(not is_equivalent_to(CustomCovariantProtocol[A], CustomCovariantProtocol[Any])) +static_assert(not is_subtype_of(CustomCovariantProtocol[A], CustomCovariantProtocol[B])) +static_assert(not is_assignable_to(CustomCovariantProtocol[A], CustomCovariantProtocol[B])) ``` ## The `Generator` protocol's `_ReturnT_co` does not need special casing as of Python 3.13 @@ -3484,8 +3490,10 @@ python-version = "3.13" ``` ```py -from ty_extensions import is_equivalent_to, is_subtype_of, static_assert -from typing import Generator +from ty_extensions import is_equivalent_to, is_subtype_of, static_assert, is_assignable_to +from typing import Generator, Awaitable, TypeVar, Protocol, Any + +T_co = TypeVar("T_co", covariant=True) class A: ... class B: ... @@ -3497,6 +3505,19 @@ static_assert(not is_subtype_of(Generator[None, None, B], Generator[None, None, static_assert(is_equivalent_to(Generator[None, None, A], Generator[None, None, A])) static_assert(is_subtype_of(Generator[None, None, A], Generator[None, None, A])) static_assert(is_subtype_of(Generator[None, None, A], Generator[None, None, A])) + +static_assert(not is_equivalent_to(Awaitable[A], Awaitable[B])) +static_assert(not is_equivalent_to(Awaitable[A], Awaitable[Any])) +static_assert(not is_subtype_of(Awaitable[A], Awaitable[B])) +static_assert(not is_assignable_to(Awaitable[A], Awaitable[B])) + +class CustomCovariantProtocol(Protocol[T_co]): + def foo(self) -> tuple[list[Generator[None, None, T_co]]]: ... + +static_assert(not is_equivalent_to(CustomCovariantProtocol[A], CustomCovariantProtocol[B])) +static_assert(not is_equivalent_to(CustomCovariantProtocol[A], CustomCovariantProtocol[Any])) +static_assert(not is_subtype_of(CustomCovariantProtocol[A], CustomCovariantProtocol[B])) +static_assert(not is_assignable_to(CustomCovariantProtocol[A], CustomCovariantProtocol[B])) ``` ## TODO diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index b512947ca4a59..ffad25785e63d 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -635,5 +635,24 @@ reveal_type(other_imported.abc) # revealed: static_assert(not is_equivalent_to(TypeOf[imported], TypeOf[other_imported])) ``` +## Bound-super types + +Two bound-super types are equivalent if the pivot class and the instance are equivalent: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from ty_extensions import is_equivalent_to, TypeOf, static_assert + +class Foo[T]: + x: T + +def bar(a: Foo[int | str], b: Foo[str | int]): + static_assert(is_equivalent_to(TypeOf[super(Foo, a)], TypeOf[super(Foo, b)])) +``` + [materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize [the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 47a8378529f2d..115c6361c4e0e 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -215,10 +215,7 @@ class EquivalentToBottom(Protocol): static_assert(is_subtype_of(EquivalentToBottom, Bottom[Callable[..., Never]])) static_assert(is_subtype_of(Bottom[Callable[..., Never]], EquivalentToBottom)) - -# TODO: is_equivalent_to only considers types of the same kind equivalent (Callable vs ProtocolInstance), -# so this fails even though mutual subtyping proves semantic equivalence. -static_assert(is_equivalent_to(Bottom[Callable[..., Never]], EquivalentToBottom)) # error: [static-assert-error] +static_assert(is_equivalent_to(Bottom[Callable[..., Never]], EquivalentToBottom)) # Top-materialized callables are not equivalent to non-top-materialized callables, even if their # signatures would otherwise be equivalent after materialization. @@ -538,6 +535,29 @@ static_assert(is_equivalent_to(Top[GenericContravariant[Any]], GenericContravari static_assert(is_equivalent_to(Bottom[GenericContravariant[Any]], GenericContravariant[object])) ``` +When all invariant type parameters are fully static (e.g. type variables rather than gradual types +like `Any`), `Top` simplifies away since there is no dynamic component to materialize: + +```py +class Foo: ... + +T_bounded = TypeVar("T_bounded", bound=Foo) +T_unbounded = TypeVar("T_unbounded") + +class InvariantBounded(Generic[T_bounded]): + x: T_bounded + +class InvariantUnbounded(Generic[T_unbounded]): + x: T_unbounded + +def f( + bounded: Top[InvariantBounded[T_bounded]], + unbounded: Top[InvariantUnbounded[T_unbounded]], +): + reveal_type(bounded) # revealed: InvariantBounded[T_bounded@f] + reveal_type(unbounded) # revealed: InvariantUnbounded[T_unbounded@f] +``` + Parameters in callable are contravariant, so the variance should be flipped: ```py diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index b382c5c974d39..c1123dd3c024a 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -1197,9 +1197,7 @@ def f(var: Foo | int): assert_type(var, Foo | int) assert_type(var, Bar | int) assert_type(var, Baz | int) - # TODO: Union simplification compares `TypedDict`s by name/identity to avoid cycles. This assert - # should also pass once that's fixed. - assert_type(var, Foo | Bar | Baz | int) # error: [type-assertion-failure] + assert_type(var, Foo | Bar | Baz | int) ``` Here are several cases that are not equivalent. In particular, assignability does not imply diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 94de60a4352ea..531472f261f41 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -22,8 +22,6 @@ use ruff_text_size::{Ranged, TextRange}; use smallvec::{SmallVec, smallvec_inline}; use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; -use type_ordering::union_or_intersection_elements_ordering; - pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder}; pub(crate) use self::class::DynamicClassLiteral; pub use self::cyclic::CycleDetector; @@ -121,7 +119,6 @@ mod special_form; mod string_annotation; mod subclass_of; mod tuple; -mod type_ordering; mod typed_dict; mod unpacker; mod variance; @@ -237,12 +234,6 @@ pub(crate) struct TryBool; pub(crate) type SpecializationVisitor<'db> = CycleDetector, ()>; pub(crate) struct VisitSpecialization; -/// A [`TypeTransformer`] that is used in `normalized` methods. -pub(crate) type NormalizedVisitor<'db> = TypeTransformer<'db, Normalized>; - -#[derive(Debug)] -pub(crate) struct Normalized; - /// How a generic type has been specialized. /// /// This matters only if there is at least one invariant type parameter. @@ -447,9 +438,7 @@ macro_rules! todo_type { } pub use crate::types::definition::TypeDefinition; -use crate::types::relation::{ - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, -}; +use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; pub(crate) use todo_type; /// Represents an instance of `builtins.property`. @@ -497,14 +486,6 @@ impl<'db> PropertyInstanceType<'db> { Self::new(db, getter, setter) } - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::new( - db, - self.getter(db).map(|ty| ty.normalized_impl(db, visitor)), - self.setter(db).map(|ty| ty.normalized_impl(db, visitor)), - ) - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -544,51 +525,6 @@ impl<'db> PropertyInstanceType<'db> { ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); } } - - fn when_equivalent_to( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { - self.is_equivalent_to_impl(db, other, inferable, &IsEquivalentVisitor::default()) - } - - fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - let getter_equivalence = if let Some(getter) = self.getter(db) { - let Some(other_getter) = other.getter(db) else { - return ConstraintSet::from(false); - }; - getter.is_equivalent_to_impl(db, other_getter, inferable, visitor) - } else { - if other.getter(db).is_some() { - return ConstraintSet::from(false); - } - ConstraintSet::from(true) - }; - - let setter_equivalence = || { - if let Some(setter) = self.setter(db) { - let Some(other_setter) = other.setter(db) else { - return ConstraintSet::from(false); - }; - setter.is_equivalent_to_impl(db, other_setter, inferable, visitor) - } else { - if other.setter(db).is_some() { - return ConstraintSet::from(false); - } - ConstraintSet::from(true) - } - }; - - getter_equivalence.and(db, setter_equivalence) - } } bitflags! { @@ -1787,105 +1723,6 @@ impl<'db> Type<'db> { } } - /// Return a "normalized" version of `self` that ensures that equivalent types have the same Salsa ID. - /// - /// A normalized type: - /// - Has all unions and intersections sorted according to a canonical order, - /// no matter how "deeply" a union/intersection may be nested. - /// - Strips the names of positional-only parameters and variadic parameters from `Callable` types, - /// as these are irrelevant to whether a callable type `X` is equivalent to a callable type `Y`. - /// - Strips the types of default values from parameters in `Callable` types: only whether a parameter - /// *has* or *does not have* a default value is relevant to whether two `Callable` types are equivalent. - /// - Converts class-based protocols into synthesized protocols - /// - Converts class-based typeddicts into synthesized typeddicts - /// - Converts all literal types to their promotable form - #[must_use] - pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { - self.normalized_impl(db, &NormalizedVisitor::default()) - } - - #[must_use] - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - Type::Union(union) => visitor.visit(self, || union.normalized_impl(db, visitor)), - Type::Intersection(intersection) => visitor.visit(self, || { - Type::Intersection(intersection.normalized_impl(db, visitor)) - }), - Type::Callable(callable) => visitor.visit(self, || { - Type::Callable(callable.normalized_impl(db, visitor)) - }), - Type::ProtocolInstance(protocol) => { - visitor.visit(self, || protocol.normalized_impl(db, visitor)) - } - Type::NominalInstance(instance) => { - visitor.visit(self, || instance.normalized_impl(db, visitor)) - } - Type::FunctionLiteral(function) => visitor.visit(self, || { - Type::FunctionLiteral(function.normalized_impl(db, visitor)) - }), - Type::PropertyInstance(property) => visitor.visit(self, || { - Type::PropertyInstance(property.normalized_impl(db, visitor)) - }), - Type::KnownBoundMethod(method_kind) => visitor.visit(self, || { - Type::KnownBoundMethod(method_kind.normalized_impl(db, visitor)) - }), - Type::BoundMethod(method) => visitor.visit(self, || { - Type::BoundMethod(method.normalized_impl(db, visitor)) - }), - Type::BoundSuper(bound_super) => visitor.visit(self, || { - Type::BoundSuper(bound_super.normalized_impl(db, visitor)) - }), - Type::GenericAlias(generic) => visitor.visit(self, || { - Type::GenericAlias(generic.normalized_impl(db, visitor)) - }), - Type::SubclassOf(subclass_of) => visitor.visit(self, || { - Type::SubclassOf(subclass_of.normalized_impl(db, visitor)) - }), - Type::TypeVar(bound_typevar) => visitor.visit(self, || { - Type::TypeVar(bound_typevar.normalized_impl(db, visitor)) - }), - Type::KnownInstance(known_instance) => visitor.visit(self, || { - Type::KnownInstance(known_instance.normalized_impl(db, visitor)) - }), - Type::TypeIs(type_is) => visitor.visit(self, || { - type_is.with_type(db, type_is.return_type(db).normalized_impl(db, visitor)) - }), - Type::TypeGuard(type_guard) => visitor.visit(self, || { - type_guard.with_type(db, type_guard.return_type(db).normalized_impl(db, visitor)) - }), - Type::Dynamic(dynamic) => Type::Dynamic(dynamic.normalized()), - Type::LiteralValue(literal) - if literal.as_enum().is_some_and(|enum_literal| { - is_single_member_enum(db, enum_literal.enum_class(db)) - }) => - { - // Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`) - literal.as_enum().unwrap().enum_class_instance(db) - } - Type::TypedDict(typed_dict) => visitor.visit(self, || { - Type::TypedDict(typed_dict.normalized_impl(db, visitor)) - }), - Type::TypeAlias(alias) => alias.value_type(db).normalized_impl(db, visitor), - Type::NewTypeInstance(newtype) => { - visitor.visit(self, || { - Type::NewTypeInstance(newtype.map_base_class_type(db, |class_type| { - class_type.normalized_impl(db, visitor) - })) - }) - } - Type::LiteralValue(literal) => Type::LiteralValue(literal.normalized_impl(db, visitor)), - Type::AlwaysFalsy - | Type::AlwaysTruthy - | Type::Never - | Type::WrapperDescriptor(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::ModuleLiteral(_) - | Type::ClassLiteral(_) - | Type::SpecialForm(_) => self, - } - } - /// Performs nest reduction for recursive types (types that contain `Divergent` types). /// For example, consider the following implicit attribute inference: /// ```python @@ -7532,40 +7369,6 @@ impl<'db> VarianceInferable<'db> for KnownInstanceType<'db> { } impl<'db> KnownInstanceType<'db> { - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - Self::SubscriptedProtocol(context) => { - Self::SubscriptedProtocol(context.normalized_impl(db, visitor)) - } - Self::SubscriptedGeneric(context) => { - Self::SubscriptedGeneric(context.normalized_impl(db, visitor)) - } - Self::TypeVar(typevar) => Self::TypeVar(typevar.normalized_impl(db, visitor)), - Self::TypeAliasType(type_alias) => Self::TypeAliasType(type_alias), - Self::Field(field) => Self::Field(field.normalized_impl(db, visitor)), - Self::UnionType(instance) => Self::UnionType(instance.normalized_impl(db, visitor)), - Self::Literal(ty) => Self::Literal(ty.normalized_impl(db, visitor)), - Self::Annotated(ty) => Self::Annotated(ty.normalized_impl(db, visitor)), - Self::TypeGenericAlias(ty) => Self::TypeGenericAlias(ty.normalized_impl(db, visitor)), - Self::Callable(callable) => Self::Callable(callable.normalized_impl(db, visitor)), - Self::LiteralStringAlias(ty) => { - Self::LiteralStringAlias(ty.normalized_impl(db, visitor)) - } - Self::NewType(newtype) => Self::NewType( - newtype - .map_base_class_type(db, |class_type| class_type.normalized_impl(db, visitor)), - ), - Self::NamedTupleSpec(spec) => Self::NamedTupleSpec(spec.normalized_impl(db, visitor)), - Self::Deprecated(_) - | Self::ConstraintSet(_) - | Self::GenericContext(_) - | Self::Specialization(_) => { - // Nothing to normalize - self - } - } - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -7721,14 +7524,6 @@ pub enum DynamicType<'db> { } impl DynamicType<'_> { - fn normalized(self) -> Self { - if matches!(self, Self::Divergent(_)) { - self - } else { - Self::Any - } - } - fn recursive_type_normalized(self) -> Self { self } @@ -8136,17 +7931,6 @@ pub struct FieldInstance<'db> { impl get_size2::GetSize for FieldInstance<'_> {} impl<'db> FieldInstance<'db> { - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - FieldInstance::new( - db, - self.default_type(db) - .map(|ty| ty.normalized_impl(db, visitor)), - self.init(db), - self.kw_only(db), - self.alias(db), - ) - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -8415,40 +8199,6 @@ impl<'db> TypeVarInstance<'db> { }) } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::new( - db, - self.identity(db), - self._bound_or_constraints(db) - .and_then(|bound_or_constraints| match bound_or_constraints { - TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { - Some(bound_or_constraints.normalized_impl(db, visitor).into()) - } - TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => { - self.lazy_bound(db).map(|bound| { - TypeVarBoundOrConstraints::UpperBound(bound) - .normalized_impl(db, visitor) - .into() - }) - } - TypeVarBoundOrConstraintsEvaluation::LazyConstraints => { - self.lazy_constraints(db).map(|constraints| { - TypeVarBoundOrConstraints::Constraints(constraints) - .normalized_impl(db, visitor) - .into() - }) - } - }), - self.explicit_variance(db), - self._default(db).and_then(|default| match default { - TypeVarDefaultEvaluation::Eager(ty) => Some(ty.normalized_impl(db, visitor).into()), - TypeVarDefaultEvaluation::Lazy => self - .lazy_default(db) - .map(|ty| ty.normalized_impl(db, visitor).into()), - }), - ) - } - fn materialize_impl( self, db: &'db dyn Db, @@ -9153,15 +8903,6 @@ impl<'db> BoundTypeVarInstance<'db> { }) } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::new( - db, - self.typevar(db).normalized_impl(db, visitor), - self.binding_context(db), - self.paramspec_attr(db), - ) - } - fn materialize_impl( self, db: &'db dyn Db, @@ -9320,15 +9061,6 @@ impl<'db> TypeVarConstraints<'db> { } } - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - let normalized = self - .elements(db) - .iter() - .map(|ty| ty.normalized_impl(db, visitor)) - .collect::>(); - TypeVarConstraints::new(db, normalized) - } - fn materialize_impl( self, db: &'db dyn Db, @@ -9388,17 +9120,6 @@ fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } impl<'db> TypeVarBoundOrConstraints<'db> { - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - TypeVarBoundOrConstraints::UpperBound(bound) => { - TypeVarBoundOrConstraints::UpperBound(bound.normalized_impl(db, visitor)) - } - TypeVarBoundOrConstraints::Constraints(constraints) => { - TypeVarBoundOrConstraints::Constraints(constraints.normalized_impl(db, visitor)) - } - } - } - fn materialize_impl( self, db: &'db dyn Db, @@ -9524,22 +9245,6 @@ impl<'db> UnionTypeInstance<'db> { } } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - let value_expr_types = self._value_expr_types(db).map(|[first, second]| { - [ - first.normalized_impl(db, visitor), - second.normalized_impl(db, visitor), - ] - }); - - let union_type = self - .union_type(db) - .clone() - .map(|ty| ty.normalized_impl(db, visitor)); - - Self::new(db, value_expr_types, union_type) - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -9589,10 +9294,6 @@ pub struct InternedType<'db> { impl get_size2::GetSize for InternedType<'_> {} impl<'db> InternedType<'db> { - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - InternedType::new(db, self.inner(db).normalized_impl(db, visitor)) - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -10630,14 +10331,6 @@ impl<'db> BoundMethodType<'db> { ) } - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::new( - db, - self.function(db).normalized_impl(db, visitor), - self.self_instance(db).normalized_impl(db, visitor), - ) - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -10686,25 +10379,6 @@ impl<'db> BoundMethodType<'db> { ) }) } - - fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - self.function(db) - .is_equivalent_to_impl(db, other.function(db), inferable, visitor) - .and(db, || { - other.self_instance(db).is_equivalent_to_impl( - db, - self.self_instance(db), - inferable, - visitor, - ) - }) - } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)] @@ -10857,17 +10531,6 @@ impl<'db> CallableType<'db> { Self::new(db, CallableSignature::bottom(), CallableTypeKind::Regular) } - /// Return a "normalized" version of this `Callable` type. - /// - /// See [`Type::normalized`] for more details. - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - CallableType::new( - db, - self.signatures(db).normalized_impl(db, visitor), - self.kind(db), - ) - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -10937,26 +10600,6 @@ impl<'db> CallableType<'db> { disjointness_visitor, ) } - - /// Check whether this callable type is equivalent to another callable type. - /// - /// See [`Type::is_equivalent_to`] for more details. - fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - if self == other { - return ConstraintSet::from(true); - } - - ConstraintSet::from(self.is_function_like(db) == other.is_function_like(db)).and(db, || { - self.signatures(db) - .is_equivalent_to_impl(db, other.signatures(db), inferable, visitor) - }) - } } /// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually, @@ -11140,7 +10783,12 @@ impl<'db> KnownBoundMethodType<'db> { | ( KnownBoundMethodType::PropertyDunderSet(self_property), KnownBoundMethodType::PropertyDunderSet(other_property), - ) => self_property.when_equivalent_to(db, other_property, inferable), + ) => Type::PropertyInstance(self_property).when_equivalent_to_impl( + db, + Type::PropertyInstance(other_property), + relation_visitor, + disjointness_visitor, + ), (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { ConstraintSet::from(self == other) @@ -11204,124 +10852,6 @@ impl<'db> KnownBoundMethodType<'db> { } } - fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - match (self, other) { - ( - KnownBoundMethodType::FunctionTypeDunderGet(self_function), - KnownBoundMethodType::FunctionTypeDunderGet(other_function), - ) => self_function.is_equivalent_to_impl(db, other_function, inferable, visitor), - - ( - KnownBoundMethodType::FunctionTypeDunderCall(self_function), - KnownBoundMethodType::FunctionTypeDunderCall(other_function), - ) => self_function.is_equivalent_to_impl(db, other_function, inferable, visitor), - - ( - KnownBoundMethodType::PropertyDunderGet(self_property), - KnownBoundMethodType::PropertyDunderGet(other_property), - ) - | ( - KnownBoundMethodType::PropertyDunderSet(self_property), - KnownBoundMethodType::PropertyDunderSet(other_property), - ) => self_property.is_equivalent_to_impl(db, other_property, inferable, visitor), - - (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { - ConstraintSet::from(self == other) - } - - ( - KnownBoundMethodType::ConstraintSetRange, - KnownBoundMethodType::ConstraintSetRange, - ) - | ( - KnownBoundMethodType::ConstraintSetAlways, - KnownBoundMethodType::ConstraintSetAlways, - ) - | ( - KnownBoundMethodType::ConstraintSetNever, - KnownBoundMethodType::ConstraintSetNever, - ) => ConstraintSet::from(true), - - ( - KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(left_constraints), - KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(right_constraints), - ) - | ( - KnownBoundMethodType::ConstraintSetSatisfies(left_constraints), - KnownBoundMethodType::ConstraintSetSatisfies(right_constraints), - ) - | ( - KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(left_constraints), - KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(right_constraints), - ) => left_constraints - .constraints(db) - .iff(db, right_constraints.constraints(db)), - - ( - KnownBoundMethodType::GenericContextSpecializeConstrained(left_generic_context), - KnownBoundMethodType::GenericContextSpecializeConstrained(right_generic_context), - ) => ConstraintSet::from(left_generic_context == right_generic_context), - - ( - KnownBoundMethodType::FunctionTypeDunderGet(_) - | KnownBoundMethodType::FunctionTypeDunderCall(_) - | KnownBoundMethodType::PropertyDunderGet(_) - | KnownBoundMethodType::PropertyDunderSet(_) - | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_), - KnownBoundMethodType::FunctionTypeDunderGet(_) - | KnownBoundMethodType::FunctionTypeDunderCall(_) - | KnownBoundMethodType::PropertyDunderGet(_) - | KnownBoundMethodType::PropertyDunderSet(_) - | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_), - ) => ConstraintSet::from(false), - } - } - - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - KnownBoundMethodType::FunctionTypeDunderGet(function) => { - KnownBoundMethodType::FunctionTypeDunderGet(function.normalized_impl(db, visitor)) - } - KnownBoundMethodType::FunctionTypeDunderCall(function) => { - KnownBoundMethodType::FunctionTypeDunderCall(function.normalized_impl(db, visitor)) - } - KnownBoundMethodType::PropertyDunderGet(property) => { - KnownBoundMethodType::PropertyDunderGet(property.normalized_impl(db, visitor)) - } - KnownBoundMethodType::PropertyDunderSet(property) => { - KnownBoundMethodType::PropertyDunderSet(property.normalized_impl(db, visitor)) - } - KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_) => self, - } - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -12440,32 +11970,6 @@ impl<'db> UnionType<'db> { } } - /// Create a new union type with the elements normalized. - /// - /// See [`Type::normalized`] for more details. - #[must_use] - pub(crate) fn normalized(self, db: &'db dyn Db) -> Type<'db> { - self.normalized_impl(db, &NormalizedVisitor::default()) - } - - pub(crate) fn normalized_impl( - self, - db: &'db dyn Db, - visitor: &NormalizedVisitor<'db>, - ) -> Type<'db> { - self.elements(db) - .iter() - .map(|ty| ty.normalized_impl(db, visitor)) - .fold( - UnionBuilder::new(db) - .order_elements(true) - .unpack_aliases(true), - UnionBuilder::add, - ) - .recursively_defined(self.recursively_defined(db)) - .build() - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -12473,7 +11977,6 @@ impl<'db> UnionType<'db> { nested: bool, ) -> Option> { let mut builder = UnionBuilder::new(db) - .order_elements(false) .unpack_aliases(false) .cycle_recovery(true) .recursively_defined(self.recursively_defined(db)); @@ -12507,33 +12010,6 @@ impl<'db> UnionType<'db> { Some(builder.build()) } - pub(crate) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - _inferable: InferableTypeVars<'_, 'db>, - _visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - if self == other { - return ConstraintSet::from(true); - } - - let self_elements = self.elements(db); - let other_elements = other.elements(db); - - if self_elements.len() != other_elements.len() { - return ConstraintSet::from(false); - } - - let sorted_self = self.normalized(db); - - if sorted_self == Type::Union(other) { - return ConstraintSet::from(true); - } - - ConstraintSet::from(sorted_self == other.normalized(db)) - } - /// Identify some specific unions of known classes, currently the ones that `float` and /// `complex` expand into in type position. pub(crate) fn known(self, db: &'db dyn Db) -> Option { @@ -12681,19 +12157,6 @@ impl<'db> NegativeIntersectionElements<'db> { } } - /// Sort the collection's types in place using the comparison function `cmp`. - pub(crate) fn sort_unstable_by( - &mut self, - compare: impl FnMut(&Type<'db>, &Type<'db>) -> std::cmp::Ordering, - ) { - match self { - Self::Empty | Self::Single(_) => {} - Self::Multiple(set) => { - set.sort_unstable_by(compare); - } - } - } - /// Remove `ty` from the collection. /// /// Returns `true` if `ty` was previously in the collection and has now been removed. @@ -12869,30 +12332,6 @@ impl<'db> IntersectionType<'db> { .build() } - /// Return a new `IntersectionType` instance with the positive and negative types sorted - /// according to a canonical ordering, and other normalizations applied to each element as applicable. - /// - /// See [`Type::normalized`] for more details. - #[must_use] - pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { - self.normalized_impl(db, &NormalizedVisitor::default()) - } - - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - let mut positive: FxOrderSet> = self - .positive(db) - .iter() - .map(|ty| ty.normalized_impl(db, visitor)) - .collect(); - - let mut negative = self.negative(db).map(|ty| ty.normalized_impl(db, visitor)); - - positive.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); - negative.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); - - IntersectionType::new(db, positive, negative) - } - pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -12927,41 +12366,6 @@ impl<'db> IntersectionType<'db> { Some(IntersectionType::new(db, positive, negative)) } - /// Return `true` if `self` represents exactly the same set of possible runtime objects as `other` - pub(crate) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - _inferable: InferableTypeVars<'_, 'db>, - _visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - if self == other { - return ConstraintSet::from(true); - } - - let self_positive = self.positive(db); - let other_positive = other.positive(db); - - if self_positive.len() != other_positive.len() { - return ConstraintSet::from(false); - } - - let self_negative = self.negative(db); - let other_negative = other.negative(db); - - if self_negative.len() != other_negative.len() { - return ConstraintSet::from(false); - } - - let sorted_self = self.normalized(db); - - if sorted_self == other { - return ConstraintSet::from(true); - } - - ConstraintSet::from(sorted_self == other.normalized(db)) - } - /// Returns an iterator over the positive elements of the intersection. If /// there are no positive elements, returns a single `object` type. pub(crate) fn positive_elements_or_object( @@ -13258,9 +12662,6 @@ pub(crate) trait TypeGuardLike<'db>: Copy { /// Get the return type that the type guard narrows to fn return_type(self, db: &'db dyn Db) -> Type<'db>; - /// Get the place info (scope and place ID) if bound - fn place_info(self, db: &'db dyn Db) -> Option<(ScopeId<'db>, ScopedPlaceId)>; - /// Get the human-readable place name if bound fn place_name(self, db: &'db dyn Db) -> Option; @@ -13278,10 +12679,6 @@ impl<'db> TypeGuardLike<'db> for TypeIsType<'db> { TypeIsType::return_type(self, db) } - fn place_info(self, db: &'db dyn Db) -> Option<(ScopeId<'db>, ScopedPlaceId)> { - TypeIsType::place_info(self, db) - } - fn place_name(self, db: &'db dyn Db) -> Option { TypeIsType::place_name(self, db) } @@ -13302,10 +12699,6 @@ impl<'db> TypeGuardLike<'db> for TypeGuardType<'db> { TypeGuardType::return_type(self, db) } - fn place_info(self, db: &'db dyn Db) -> Option<(ScopeId<'db>, ScopedPlaceId)> { - TypeGuardType::place_info(self, db) - } - fn place_name(self, db: &'db dyn Db) -> Option { TypeGuardType::place_name(self, db) } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 5c8409766844a..f55dc2d5722ba 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -9,11 +9,12 @@ use crate::{ place::{Place, PlaceAndQualifiers}, types::{ BoundTypeVarInstance, ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, - MemberLookupPolicy, NominalInstanceType, NormalizedVisitor, SpecialFormType, - SubclassOfInner, SubclassOfType, Type, TypeVarBoundOrConstraints, TypeVarConstraints, - TypeVarInstance, UnionBuilder, + MemberLookupPolicy, NominalInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, + Type, TypeVarBoundOrConstraints, TypeVarConstraints, TypeVarInstance, UnionBuilder, + constraints::ConstraintSet, context::InferContext, diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS}, + relation::{HasRelationToVisitor, IsDisjointVisitor}, todo_type, visitor, }, }; @@ -195,30 +196,6 @@ pub enum SuperOwnerKind<'db> { } impl<'db> SuperOwnerKind<'db> { - fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - SuperOwnerKind::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic.normalized()), - SuperOwnerKind::Class(class) => { - SuperOwnerKind::Class(class.normalized_impl(db, visitor)) - } - SuperOwnerKind::Instance(instance) => instance - .normalized_impl(db, visitor) - .as_nominal_instance() - .map(Self::Instance) - .unwrap_or(Self::Dynamic(DynamicType::Any)), - SuperOwnerKind::InstanceTypeVar(bound_typevar, class) => { - SuperOwnerKind::InstanceTypeVar( - bound_typevar.normalized_impl(db, visitor), - class.normalized_impl(db, visitor), - ) - } - SuperOwnerKind::ClassTypeVar(bound_typevar, class) => SuperOwnerKind::ClassTypeVar( - bound_typevar.normalized_impl(db, visitor), - class.normalized_impl(db, visitor), - ), - } - } - fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -747,14 +724,6 @@ impl<'db> BoundSuperType<'db> { } } - pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::new( - db, - self.pivot_class(db).normalized_impl(db, visitor), - self.owner(db).normalized_impl(db, visitor), - ) - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -769,4 +738,109 @@ impl<'db> BoundSuperType<'db> { .recursive_type_normalized_impl(db, div, nested)?, )) } + + /// Check whether two `BoundSuperType`s are equivalent by recursing into + /// their fields. + /// + /// Despite the name, this is called from `Type::has_relation_to_impl`, + /// not from `Type::is_equivalent_to_impl`. `Type::has_relation_to_impl` + /// cannot simply delegate to `Type::is_equivalent_to_impl` for this + /// case, because `Type::is_equivalent_to_impl` itself delegates back to + /// `Type::has_relation_to_impl`, which would cause an infinite loop. + pub(crate) fn is_equivalent_to_impl( + self, + db: &'db dyn Db, + other: Self, + relation_visitor: &HasRelationToVisitor<'db>, + disjointness_visitor: &IsDisjointVisitor<'db>, + ) -> ConstraintSet<'db> { + let mut class_equivalence = match (self.pivot_class(db), other.pivot_class(db)) { + (ClassBase::Class(left), ClassBase::Class(right)) => Type::from(left) + .when_equivalent_to_impl( + db, + Type::from(right), + relation_visitor, + disjointness_visitor, + ), + (ClassBase::Class(_), _) => ConstraintSet::from(false), + + // A `Divergent` type is only equivalent to itself + ( + ClassBase::Dynamic(DynamicType::Divergent(l)), + ClassBase::Dynamic(DynamicType::Divergent(r)), + ) => ConstraintSet::from(l == r), + (ClassBase::Dynamic(DynamicType::Divergent(_)), _) + | (_, ClassBase::Dynamic(DynamicType::Divergent(_))) => ConstraintSet::from(false), + (ClassBase::Dynamic(_), ClassBase::Dynamic(_)) => ConstraintSet::from(true), + (ClassBase::Dynamic(_), _) => ConstraintSet::from(false), + + (ClassBase::Generic, ClassBase::Generic) => ConstraintSet::from(true), + (ClassBase::Generic, _) => ConstraintSet::from(false), + + (ClassBase::Protocol, ClassBase::Protocol) => ConstraintSet::from(true), + (ClassBase::Protocol, _) => ConstraintSet::from(false), + + (ClassBase::TypedDict, ClassBase::TypedDict) => ConstraintSet::from(true), + (ClassBase::TypedDict, _) => ConstraintSet::from(false), + }; + if class_equivalence.is_never_satisfied(db) { + return ConstraintSet::from(false); + } + let owner_equivalence = match (self.owner(db), other.owner(db)) { + (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => Type::from(left) + .when_equivalent_to_impl( + db, + Type::from(right), + relation_visitor, + disjointness_visitor, + ), + (SuperOwnerKind::Class(_), _) => ConstraintSet::from(false), + + (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => Type::from(left) + .when_equivalent_to_impl( + db, + Type::from(right), + relation_visitor, + disjointness_visitor, + ), + (SuperOwnerKind::Instance(_), _) => ConstraintSet::from(false), + + // A `Divergent` type is only equivalent to itself + ( + SuperOwnerKind::Dynamic(DynamicType::Divergent(l)), + SuperOwnerKind::Dynamic(DynamicType::Divergent(r)), + ) => ConstraintSet::from(l == r), + (SuperOwnerKind::Dynamic(DynamicType::Divergent(_)), _) + | (_, SuperOwnerKind::Dynamic(DynamicType::Divergent(_))) => ConstraintSet::from(false), + (SuperOwnerKind::Dynamic(_), SuperOwnerKind::Dynamic(_)) => ConstraintSet::from(true), + (SuperOwnerKind::Dynamic(_), _) => ConstraintSet::from(false), + + ( + SuperOwnerKind::InstanceTypeVar(l_typevar, l_class), + SuperOwnerKind::InstanceTypeVar(r_typevar, r_class), + ) + | ( + SuperOwnerKind::ClassTypeVar(l_typevar, l_class), + SuperOwnerKind::ClassTypeVar(r_typevar, r_class), + ) => Type::TypeVar(l_typevar) + .when_equivalent_to_impl( + db, + Type::TypeVar(r_typevar), + relation_visitor, + disjointness_visitor, + ) + .and(db, || { + Type::from(l_class).when_equivalent_to_impl( + db, + Type::from(r_class), + relation_visitor, + disjointness_visitor, + ) + }), + (SuperOwnerKind::InstanceTypeVar(..) | SuperOwnerKind::ClassTypeVar(..), _) => { + ConstraintSet::from(false) + } + }; + class_equivalence.intersect(db, owner_equivalence) + } } diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index c779ab0c1f95d..58dba0bf97343 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -37,7 +37,6 @@ //! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks. use crate::types::enums::{enum_member_literals, enum_metadata}; -use crate::types::type_ordering::union_or_intersection_elements_ordering; use crate::types::{ BytesLiteralType, ClassLiteral, EnumLiteralType, IntersectionType, KnownClass, LiteralValueType, LiteralValueTypeKind, NegativeIntersectionElements, StringLiteralType, Type, @@ -270,7 +269,6 @@ pub(crate) struct UnionBuilder<'db> { elements: Vec>, db: &'db dyn Db, unpack_aliases: bool, - order_elements: bool, /// This is enabled when joining types in a `cycle_recovery` function. /// Since a cycle cannot be created within a `cycle_recovery` function, /// execution of `is_redundant_with` is skipped. @@ -284,7 +282,6 @@ impl<'db> UnionBuilder<'db> { db, elements: vec![], unpack_aliases: true, - order_elements: false, cycle_recovery: false, recursively_defined: RecursivelyDefined::No, } @@ -295,11 +292,6 @@ impl<'db> UnionBuilder<'db> { self } - pub(crate) fn order_elements(mut self, val: bool) -> Self { - self.order_elements = val; - self - } - pub(crate) fn cycle_recovery(mut self, val: bool) -> Self { self.cycle_recovery = val; if self.cycle_recovery { @@ -781,9 +773,6 @@ impl<'db> UnionBuilder<'db> { UnionElement::Type(ty) => types.push(ty), } } - if self.order_elements { - types.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(self.db, l, r)); - } match types.len() { 0 => None, 1 => Some(types[0]), @@ -804,7 +793,6 @@ pub(crate) struct IntersectionBuilder<'db> { // but if a union is added to the intersection, we'll distribute ourselves over that union and // create a union of intersections. intersections: Vec>, - order_elements: bool, db: &'db dyn Db, } @@ -812,7 +800,6 @@ impl<'db> IntersectionBuilder<'db> { pub(crate) fn new(db: &'db dyn Db) -> Self { Self { db, - order_elements: false, intersections: vec![InnerIntersectionBuilder::default()], } } @@ -820,7 +807,6 @@ impl<'db> IntersectionBuilder<'db> { fn empty(db: &'db dyn Db) -> Self { Self { db, - order_elements: false, intersections: vec![], } } @@ -1024,7 +1010,6 @@ impl<'db> IntersectionBuilder<'db> { // For enum-containing intersections, add the remaining members as positive let mut enum_builder = IntersectionBuilder { db, - order_elements: self.order_elements, intersections: enum_intersections, } .add_positive_impl(remaining_members, seen_aliases); @@ -1032,7 +1017,6 @@ impl<'db> IntersectionBuilder<'db> { // For non-enum intersections, just add the negative normally let mut other_builder = IntersectionBuilder { db, - order_elements: self.order_elements, intersections: other_intersections, }; for inner in &mut other_builder.intersections { @@ -1077,16 +1061,13 @@ impl<'db> IntersectionBuilder<'db> { pub(crate) fn build(mut self) -> Type<'db> { // Avoid allocating the UnionBuilder unnecessarily if we have just one intersection: if self.intersections.len() == 1 { - self.intersections - .pop() - .unwrap() - .build(self.db, self.order_elements) + self.intersections.pop().unwrap().build(self.db) } else { UnionType::from_elements( self.db, self.intersections .into_iter() - .map(|inner| inner.build(self.db, self.order_elements)), + .map(|inner| inner.build(self.db)), ) } } @@ -1473,7 +1454,7 @@ impl<'db> InnerIntersectionBuilder<'db> { } } - fn build(mut self, db: &'db dyn Db, order_elements: bool) -> Type<'db> { + fn build(mut self, db: &'db dyn Db) -> Type<'db> { self.simplify_constrained_typevars(db); // If any typevars are in `self.positive`, speculatively solve all bounded type variables @@ -1514,12 +1495,6 @@ impl<'db> InnerIntersectionBuilder<'db> { _ => { self.positive.shrink_to_fit(); self.negative.shrink_to_fit(); - if order_elements { - self.positive - .sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); - self.negative - .sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); - } Type::Intersection(IntersectionType::new(db, self.positive, self.negative)) } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e1806c2018267..9fea57b4d7d25 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1146,8 +1146,7 @@ impl<'db> Bindings<'db> { Type::FunctionLiteral(function_type) => match function_type.known(db) { Some(KnownFunction::IsEquivalentTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let constraints = - ty_a.when_equivalent_to(db, *ty_b, InferableTypeVars::None); + let constraints = ty_a.when_equivalent_to(db, *ty_b); let tracked = InternedConstraintSet::new(db, constraints); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), @@ -2482,14 +2481,7 @@ impl<'db> CallableBinding<'db> { overload.signature.parameters()[parameter_index].annotated_type(); let first_parameter_type = &mut first_parameter_types[parameter_index]; if let Some(first_parameter_type) = first_parameter_type { - if !first_parameter_type - .when_equivalent_to( - db, - current_parameter_type, - overload.inferable_typevars, - ) - .is_always_satisfied(db) - { + if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { participating_parameter_indexes.insert(parameter_index); } } else { @@ -2630,12 +2622,7 @@ impl<'db> CallableBinding<'db> { matching_overloads.all(|(_, overload)| { overload .return_type() - .when_equivalent_to( - db, - first_overload_return_type, - overload.inferable_typevars, - ) - .is_always_satisfied(db) + .is_equivalent_to(db, first_overload_return_type) }) } else { // No matching overload diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index bedcc2845b9fa..90ef014b08310 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -33,9 +33,7 @@ use crate::types::generics::{ use crate::types::infer::{infer_expression_type, infer_unpack_types, nearest_enclosing_class}; use crate::types::member::{Member, class_member}; use crate::types::mro::DynamicMroError; -use crate::types::relation::{ - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, -}; +use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::tuple::{Tuple, TupleSpec, TupleType}; use crate::types::typed_dict::typed_dict_params_from_class_def; @@ -44,9 +42,8 @@ use crate::types::{ ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType, CallableTypeKind, CallableTypes, DATACLASS_FLAGS, DataclassFlags, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownInstanceType, - MaterializationKind, NormalizedVisitor, PropertyInstanceType, TypeContext, TypeMapping, - TypedDictParams, UnionBuilder, VarianceInferable, binding_type, declaration_type, - determine_upper_bound, + MaterializationKind, PropertyInstanceType, TypeContext, TypeMapping, TypedDictParams, + UnionBuilder, VarianceInferable, binding_type, declaration_type, determine_upper_bound, }; use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, @@ -293,14 +290,6 @@ pub(super) fn walk_generic_alias<'db, V: super::visitor::TypeVisitor<'db> + ?Siz impl get_size2::GetSize for GenericAlias<'_> {} impl<'db> GenericAlias<'db> { - pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::new( - db, - self.origin(db), - self.specialization(db).normalized_impl(db, visitor), - ) - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -848,13 +837,6 @@ impl<'db> ClassType<'db> { } } - pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - Self::NonGeneric(_) => self, - Self::Generic(generic) => Self::Generic(generic.normalized_impl(db, visitor)), - } - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -1189,7 +1171,7 @@ impl<'db> ClassType<'db> { match base { ClassBase::Dynamic(_) => match relation { TypeRelation::Subtyping - | TypeRelation::Redundancy + | TypeRelation::Redundancy { .. } | TypeRelation::SubtypingAssuming(_) => { ConstraintSet::from(other.is_object(db)) } @@ -1233,37 +1215,6 @@ impl<'db> ClassType<'db> { }) } - pub(super) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: ClassType<'db>, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - if self == other { - return ConstraintSet::from(true); - } - - match (self, other) { - // Two non-generic classes are only equivalent if they are equal (handled above). - // A non-generic class is never equivalent to a generic class. - (ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => { - ConstraintSet::from(false) - } - - (ClassType::Generic(this), ClassType::Generic(other)) => { - ConstraintSet::from(this.origin(db) == other.origin(db)).and(db, || { - this.specialization(db).is_equivalent_to_impl( - db, - other.specialization(db), - inferable, - visitor, - ) - }) - } - } - } - /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { match self { @@ -6077,20 +6028,6 @@ impl<'db> NamedTupleSpec<'db> { Self::new(db, Box::default(), false) } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - let fields: Box<_> = self - .fields(db) - .iter() - .map(|f| NamedTupleField { - name: f.name.clone(), - ty: f.ty.normalized_impl(db, visitor), - default: None, - }) - .collect(); - - Self::new(db, fields, self.has_known_fields(db)) - } - pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index a8f1ea4722fe0..520846c731c1f 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -4,8 +4,8 @@ use crate::types::mro::MroIterator; use crate::types::tuple::TupleType; use crate::types::{ ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType, - MaterializationKind, NormalizedVisitor, SpecialFormType, StaticMroError, Type, TypeContext, - TypeMapping, todo_type, + MaterializationKind, SpecialFormType, StaticMroError, Type, TypeContext, TypeMapping, + todo_type, }; use crate::{Db, DisplaySettings}; @@ -36,14 +36,6 @@ impl<'db> ClassBase<'db> { Self::Dynamic(DynamicType::Unknown) } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), - Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)), - Self::Protocol | Self::Generic | Self::TypedDict => self, - } - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index de0c341337333..d182a228f0d45 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -709,15 +709,6 @@ impl<'db> ConstrainedTypeVar<'db> { ConstraintAssignment::Negative(self) } - fn normalized(self, db: &'db dyn Db) -> Self { - Self::new( - db, - self.typevar(db), - self.lower(db).normalized(db), - self.upper(db).normalized(db), - ) - } - /// Defines the ordering of the variables in a constraint set BDD. /// /// If we only care about _correctness_, we can choose any ordering that we want, as long as @@ -1843,7 +1834,7 @@ impl<'db> Node<'db> { Node::AlwaysFalse => {} Node::AlwaysTrue => self.clauses.push(self.current_clause.clone()), Node::Interior(interior) => { - let interior_constraint = interior.constraint(db).normalized(db); + let interior_constraint = interior.constraint(db); self.current_clause.push(interior_constraint.when_true()); self.visit_node(db, interior.if_true(db)); self.current_clause.pop(); @@ -2729,8 +2720,6 @@ impl<'db> InteriorNode<'db> { // non-empty. match left_constraint.intersect(db, right_constraint) { IntersectionResult::Simplified(intersection_constraint) => { - let intersection_constraint = intersection_constraint.normalized(db); - // If the intersection is non-empty, we need to create a new constraint to // represent that intersection. We also need to add the new constraint to our // seen set and (if we haven't already seen it) to the to-visit queue. diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index cab6d46f29d49..f29da948a65c1 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -2971,11 +2971,7 @@ mod tests { use crate::Db; use crate::db::tests::setup_db; - use crate::place::typing_extensions_symbol; - use crate::types::typed_dict::{ - SynthesizedTypedDictType, TypedDictFieldBuilder, TypedDictSchema, - }; - use crate::types::{KnownClass, Parameter, Parameters, Signature, Type, TypedDictType}; + use crate::types::{KnownClass, Parameter, Parameters, Signature, Type}; #[test] fn string_literal_display() { @@ -2995,62 +2991,6 @@ mod tests { ); } - #[test] - fn synthesized_protocol_display() { - let db = setup_db(); - - // Call `.normalized()` to turn the class-based protocol into a nameless synthesized one. - let supports_index_synthesized = KnownClass::SupportsIndex.to_instance(&db).normalized(&db); - assert_eq!( - supports_index_synthesized.display(&db).to_string(), - "" - ); - - let iterator_synthesized = typing_extensions_symbol(&db, "Iterator") - .place - .ignore_possibly_undefined() - .unwrap() - .to_instance(&db) - .unwrap() - .normalized(&db); // Call `.normalized()` to turn the class-based protocol into a nameless synthesized one. - - assert_eq!( - iterator_synthesized.display(&db).to_string(), - "" - ); - } - - #[test] - fn synthesized_typeddict_display() { - let db = setup_db(); - - let mut items = TypedDictSchema::default(); - items.insert( - Name::new("foo"), - TypedDictFieldBuilder::new(Type::int_literal(42)) - .required(true) - .build(), - ); - items.insert( - Name::new("bar"), - TypedDictFieldBuilder::new(Type::string_literal(&db, "hello")) - .required(true) - .build(), - ); - - let synthesized = SynthesizedTypedDictType::new(&db, items); - let type_ = Type::TypedDict(TypedDictType::Synthesized(synthesized)); - // Fields are sorted internally, even prior to normalization. - assert_eq!( - type_.display(&db).to_string(), - "", - ); - assert_eq!( - type_.normalized(&db).display(&db).to_string(), - "", - ); - } - fn display_signature<'db>( db: &'db dyn Db, parameters: impl IntoIterator>, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 20a7b574c5e8b..53d30e551d694 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -80,18 +80,15 @@ use crate::types::generics::{GenericContext, InferableTypeVars, typing_self}; use crate::types::infer::nearest_enclosing_class; use crate::types::list_members::all_members; use crate::types::narrow::ClassInfoConstraintFunction; -use crate::types::relation::{ - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, -}; +use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; use crate::types::{ ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, CallableTypeKind, ClassBase, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, - KnownClass, KnownInstanceType, NormalizedVisitor, SpecialFormType, SubclassOfInner, - SubclassOfType, Truthiness, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, - UnionBuilder, UnionType, binding_type, definition_expression_type, infer_definition_types, - walk_signature, + KnownClass, KnownInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, + Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, UnionBuilder, UnionType, + binding_type, definition_expression_type, infer_definition_types, walk_signature, }; use crate::{Db, FxOrderSet}; @@ -1229,24 +1226,6 @@ impl<'db> FunctionType<'db> { ) } - pub(crate) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - if self.normalized(db) == other.normalized(db) { - return ConstraintSet::from(true); - } - if self.literal(db) != other.literal(db) { - return ConstraintSet::from(false); - } - let self_signature = self.signature(db); - let other_signature = other.signature(db); - self_signature.is_equivalent_to_impl(db, other_signature, inferable, visitor) - } - pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, @@ -1260,26 +1239,6 @@ impl<'db> FunctionType<'db> { } } - pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { - self.normalized_impl(db, &NormalizedVisitor::default()) - } - - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - let literal = self.literal(db); - let updated_signature = self - .updated_signature(db) - .map(|signature| signature.normalized_impl(db, visitor)); - let updated_last_definition_signature = self - .updated_last_definition_signature(db) - .map(|signature| signature.normalized_impl(db, visitor)); - Self::new( - db, - literal, - updated_signature, - updated_last_definition_signature, - ) - } - pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index c5802e465faf9..d6a7eef824650 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -15,9 +15,7 @@ use crate::semantic_index::{SemanticIndex, semantic_index}; use crate::types::class::ClassType; use crate::types::class_base::ClassBase; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension, Solutions}; -use crate::types::relation::{ - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, -}; +use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Parameters}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::variance::VarianceInferable; @@ -25,9 +23,9 @@ use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, CallableType, CallableTypes, ClassLiteral, FindLegacyTypeVarsVisitor, IntersectionType, - KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeAliasType, - TypeContext, TypeMapping, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, - TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_callable_type, + KnownClass, KnownInstanceType, MaterializationKind, Type, TypeAliasType, TypeContext, + TypeMapping, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, + TypeVarVariance, UnionType, declaration_type, walk_callable_type, walk_manual_pep_695_type_alias, walk_pep_695_type_alias, walk_type_var_bounds, }; use crate::{Db, FxIndexMap, FxOrderMap, FxOrderSet}; @@ -953,14 +951,6 @@ impl<'db> GenericContext<'db> { { Specialization::new(db, self, self.fill_in_defaults(db, types), None, None) } - - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - let variables = self - .variables(db) - .map(|bound_typevar| bound_typevar.normalized_impl(db, visitor)); - - Self::from_typevar_instances(db, variables) - } } /// An assignment of a specific type to each type variable in a generic scope. @@ -1146,7 +1136,9 @@ fn has_relation_in_invariant_position<'db>( ( None, Some(base_mat), - TypeRelation::Subtyping | TypeRelation::Redundancy | TypeRelation::SubtypingAssuming(_), + TypeRelation::Subtyping + | TypeRelation::Redundancy { .. } + | TypeRelation::SubtypingAssuming(_), ) => is_subtype_in_invariant_position( db, derived_type, @@ -1160,7 +1152,9 @@ fn has_relation_in_invariant_position<'db>( ( Some(derived_mat), None, - TypeRelation::Subtyping | TypeRelation::Redundancy | TypeRelation::SubtypingAssuming(_), + TypeRelation::Subtyping + | TypeRelation::Redundancy { .. } + | TypeRelation::SubtypingAssuming(_), ) => is_subtype_in_invariant_position( db, derived_type, @@ -1381,25 +1375,6 @@ impl<'db> Specialization<'db> { Specialization::new(db, self.generic_context(db), types, None, None) } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - let types: Box<[_]> = self - .types(db) - .iter() - .map(|ty| ty.normalized_impl(db, visitor)) - .collect(); - let tuple_inner = self - .tuple_inner(db) - .and_then(|tuple| tuple.normalized_impl(db, visitor)); - let context = self.generic_context(db).normalized_impl(db, visitor); - Self::new( - db, - context, - types, - self.materialization_kind(db), - tuple_inner, - ) - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -1646,61 +1621,6 @@ impl<'db> Specialization<'db> { ) } - pub(crate) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Specialization<'db>, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - if self.materialization_kind(db) != other.materialization_kind(db) { - return ConstraintSet::from(false); - } - let generic_context = self.generic_context(db); - if generic_context != other.generic_context(db) { - return ConstraintSet::from(false); - } - - let mut result = ConstraintSet::from(true); - for ((bound_typevar, self_type), other_type) in generic_context - .variables(db) - .zip(self.types(db)) - .zip(other.types(db)) - { - // Equivalence of each type in the specialization depends on the variance of the - // corresponding typevar: - // - covariant: verify that self_type == other_type - // - contravariant: verify that other_type == self_type - // - invariant: verify that self_type == other_type - // - bivariant: skip, can't make equivalence false - let compatible = match bound_typevar.variance(db) { - TypeVarVariance::Invariant - | TypeVarVariance::Covariant - | TypeVarVariance::Contravariant => { - self_type.is_equivalent_to_impl(db, *other_type, inferable, visitor) - } - TypeVarVariance::Bivariant => ConstraintSet::from(true), - }; - if result.intersect(db, compatible).is_never_satisfied(db) { - return result; - } - } - - match (self.tuple_inner(db), other.tuple_inner(db)) { - (Some(_), None) | (None, Some(_)) => return ConstraintSet::from(false), - (None, None) => {} - (Some(self_tuple), Some(other_tuple)) => { - let compatible = - self_tuple.is_equivalent_to_impl(db, other_tuple, inferable, visitor); - if result.intersect(db, compatible).is_never_satisfied(db) { - return result; - } - } - } - - result - } - pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index c84378248cd6c..bd2a5c9757a59 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -15,13 +15,11 @@ use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::enums::is_single_member_enum; use crate::types::generics::{InferableTypeVars, walk_specialization}; use crate::types::protocol_class::{ProtocolClass, walk_protocol_interface}; -use crate::types::relation::{ - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, -}; +use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::{ ApplyTypeMappingVisitor, ClassBase, ClassLiteral, FindLegacyTypeVarsVisitor, - LiteralValueTypeKind, NormalizedVisitor, TypeContext, TypeMapping, VarianceInferable, + LiteralValueTypeKind, TypeContext, TypeMapping, VarianceInferable, }; use crate::{Db, FxOrderSet, Program}; pub(super) use synthesized_protocol::SynthesizedProtocolType; @@ -134,11 +132,7 @@ impl<'db> Type<'db> { M: IntoIterator)>, { Self::ProtocolInstance(ProtocolInstanceType::synthesized( - SynthesizedProtocolType::new( - db, - ProtocolInterface::with_property_members(db, members), - &NormalizedVisitor::default(), - ), + SynthesizedProtocolType::new(ProtocolInterface::with_property_members(db, members)), )) } @@ -417,22 +411,6 @@ impl<'db> NominalInstanceType<'db> { }) } - pub(super) fn normalized_impl( - self, - db: &'db dyn Db, - visitor: &NormalizedVisitor<'db>, - ) -> Type<'db> { - match self.0 { - NominalInstanceInner::ExactTuple(tuple) => { - Type::tuple(tuple.normalized_impl(db, visitor)) - } - NominalInstanceInner::NonTuple(class) => Type::NominalInstance(NominalInstanceType( - NominalInstanceInner::NonTuple(class.normalized_impl(db, visitor)), - )), - NominalInstanceInner::Object => Type::object(), - } - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -485,28 +463,6 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - match (self.0, other.0) { - ( - NominalInstanceInner::ExactTuple(tuple1), - NominalInstanceInner::ExactTuple(tuple2), - ) => tuple1.is_equivalent_to_impl(db, tuple2, inferable, visitor), - (NominalInstanceInner::Object, NominalInstanceInner::Object) => { - ConstraintSet::from(true) - } - (NominalInstanceInner::NonTuple(class1), NominalInstanceInner::NonTuple(class2)) => { - class1.is_equivalent_to_impl(db, class2, inferable, visitor) - } - _ => ConstraintSet::from(false), - } - } - pub(super) fn is_disjoint_from_impl( self, db: &'db dyn Db, @@ -773,32 +729,6 @@ impl<'db> ProtocolInstanceType<'db> { is_equivalent_to_object_inner(db, self, ()) } - /// Return a "normalized" version of this `Protocol` type. - /// - /// See [`Type::normalized`] for more details. - pub(super) fn normalized(self, db: &'db dyn Db) -> Type<'db> { - self.normalized_impl(db, &NormalizedVisitor::default()) - } - - /// Return a "normalized" version of this `Protocol` type. - /// - /// See [`Type::normalized`] for more details. - pub(super) fn normalized_impl( - self, - db: &'db dyn Db, - visitor: &NormalizedVisitor<'db>, - ) -> Type<'db> { - if self.is_equivalent_to_object(db) { - return Type::object(); - } - match self.inner { - Protocol::FromClass(_) => Type::ProtocolInstance(Self::synthesized( - SynthesizedProtocolType::new(db, self.inner.interface(db), visitor), - )), - Protocol::Synthesized(_) => Type::ProtocolInstance(self), - } - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -811,40 +741,6 @@ impl<'db> ProtocolInstanceType<'db> { }) } - /// Return `true` if this protocol type is equivalent to the protocol `other`. - /// - /// TODO: consider the types of the members as well as their existence - pub(super) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - if self == other { - return ConstraintSet::from(true); - } - - // `Generator` special case: Prior to 3.13, the `_ReturnT_co` type didn't appear in any - // methods (except `__iter__`, but that returns the self type recursively, so it can't rule - // out equivalence). We don't want generators with different return types to be equivalent - // to each other. In this case we compare the `ClassType`s nominally. - if let Protocol::FromClass(self_class) = self.inner - && let Protocol::FromClass(other_class) = other.inner - && self_class.known(db) == Some(KnownClass::Generator) - && other_class.known(db) == Some(KnownClass::Generator) - && Program::get(db).python_version(db) < PythonVersion::PY313 - { - return (*self_class).is_equivalent_to_impl(db, *other_class, inferable, visitor); - } - - let self_normalized = self.normalized(db); - if self_normalized == Type::ProtocolInstance(other) { - return ConstraintSet::from(true); - } - ConstraintSet::from(self_normalized == other.normalized(db)) - } - /// Return `true` if this protocol type is disjoint from the protocol `other`. /// /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y` @@ -969,32 +865,20 @@ mod synthesized_protocol { use crate::semantic_index::definition::Definition; use crate::types::protocol_class::ProtocolInterface; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, - NormalizedVisitor, Type, TypeContext, TypeMapping, TypeVarVariance, VarianceInferable, + ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, Type, + TypeContext, TypeMapping, TypeVarVariance, VarianceInferable, }; use crate::{Db, FxOrderSet}; /// A "synthesized" protocol type that is dissociated from a class definition in source code. - /// - /// Two synthesized protocol types with the same members will share the same Salsa ID, - /// making them easy to compare for equivalence. A synthesized protocol type is therefore - /// returned by [`super::ProtocolInstanceType::normalized`] so that two protocols with the same members - /// will be understood as equivalent even in the context of differently ordered unions or intersections. - /// - /// The constructor method of this type maintains the invariant that a synthesized protocol type - /// is always constructed from a *normalized* protocol interface. #[derive( Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, )] pub(in crate::types) struct SynthesizedProtocolType<'db>(ProtocolInterface<'db>); impl<'db> SynthesizedProtocolType<'db> { - pub(super) fn new( - db: &'db dyn Db, - interface: ProtocolInterface<'db>, - visitor: &NormalizedVisitor<'db>, - ) -> Self { - Self(interface.normalized_impl(db, visitor)) + pub(super) fn new(interface: ProtocolInterface<'db>) -> Self { + Self(interface) } pub(super) fn apply_type_mapping_impl<'a>( @@ -1002,9 +886,12 @@ mod synthesized_protocol { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - _visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { - Self(self.0.specialized_and_normalized(db, type_mapping, tcx)) + Self( + self.0 + .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + ) } pub(super) fn find_legacy_typevars_impl( diff --git a/crates/ty_python_semantic/src/types/literal.rs b/crates/ty_python_semantic/src/types/literal.rs index 6282d0e86b4aa..57aa957f873aa 100644 --- a/crates/ty_python_semantic/src/types/literal.rs +++ b/crates/ty_python_semantic/src/types/literal.rs @@ -2,7 +2,7 @@ use compact_str::CompactString; use ruff_python_ast::name::Name; use crate::Db; -use crate::types::{ClassLiteral, KnownClass, NormalizedVisitor, Type}; +use crate::types::{ClassLiteral, KnownClass, Type}; /// A literal value. See [`LiteralValueTypeKind`] for details. #[derive( @@ -93,37 +93,6 @@ impl<'db> LiteralValueType<'db> { Self(repr) } - /// Returns the promotable form of this literal value. - #[must_use] - pub(crate) fn to_promotable(self) -> Self { - let repr = match self.0 { - LiteralValueTypeInner::UnpromotableInt(int) => { - LiteralValueTypeInner::PromotableInt(int) - } - LiteralValueTypeInner::UnpromotableBool(bool) => { - LiteralValueTypeInner::PromotableBool(bool) - } - LiteralValueTypeInner::UnpromotableString(string) => { - LiteralValueTypeInner::PromotableString(string) - } - LiteralValueTypeInner::UnpromotableEnum(e) => LiteralValueTypeInner::PromotableEnum(e), - LiteralValueTypeInner::UnpromotableBytes(bytes) => { - LiteralValueTypeInner::PromotableBytes(bytes) - } - LiteralValueTypeInner::UnpromotableLiteralString => { - LiteralValueTypeInner::PromotableLiteralString - } - LiteralValueTypeInner::PromotableInt(_) - | LiteralValueTypeInner::PromotableBool(_) - | LiteralValueTypeInner::PromotableString(_) - | LiteralValueTypeInner::PromotableEnum(_) - | LiteralValueTypeInner::PromotableBytes(_) - | LiteralValueTypeInner::PromotableLiteralString => self.0, - }; - - Self(repr) - } - /// Returns the unpromotable form of this literal value. #[must_use] pub(crate) fn to_unpromotable(self) -> Self { @@ -268,14 +237,6 @@ impl<'db> LiteralValueType<'db> { LiteralValueTypeKind::Enum(literal) => literal.enum_class_instance(db), } } - - pub(crate) fn normalized_impl( - self, - _db: &'db dyn Db, - _visitor: &NormalizedVisitor<'db>, - ) -> Self { - self.to_promotable() - } } impl From for LiteralValueTypeKind<'_> { diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 4fcfbdf87556b..1f18b68812fc9 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -18,8 +18,8 @@ use crate::{ types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassType, FindLegacyTypeVarsVisitor, InstanceFallbackShadowsNonDataDescriptor, KnownFunction, - MemberLookupPolicy, NormalizedVisitor, PropertyInstanceType, Signature, StaticClassLiteral, - Type, TypeMapping, TypeQualifiers, TypeVarVariance, VarianceInferable, + MemberLookupPolicy, PropertyInstanceType, Signature, StaticClassLiteral, Type, TypeMapping, + TypeQualifiers, TypeVarVariance, VarianceInferable, constraints::{ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension}, context::InferContext, diagnostic::report_undeclared_protocol_member, @@ -226,7 +226,7 @@ impl<'db> ProtocolInterface<'db> { db, [Parameter::positional_only(Some(Name::new_static("self")))], ), - ty.normalized(db), + ty, ); let property_getter = Type::single_callable(db, property_getter_signature); let property = PropertyInstanceType::new(db, Some(property_getter), None); @@ -399,16 +399,6 @@ impl<'db> ProtocolInterface<'db> { }) } - pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::new( - db, - self.inner(db) - .iter() - .map(|(name, data)| (name.clone(), data.normalized_impl(db, visitor))) - .collect::>(), - ) - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -429,11 +419,12 @@ impl<'db> ProtocolInterface<'db> { )) } - pub(super) fn specialized_and_normalized<'a>( + pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { Self::new( db, @@ -442,13 +433,7 @@ impl<'db> ProtocolInterface<'db> { .map(|(name, data)| { ( name.clone(), - data.apply_type_mapping_impl( - db, - type_mapping, - tcx, - &ApplyTypeMappingVisitor::default(), - ) - .normalized(db), + data.apply_type_mapping_impl(db, type_mapping, tcx, visitor), ) }) .collect::>(), @@ -510,18 +495,6 @@ pub(super) struct ProtocolMemberData<'db> { } impl<'db> ProtocolMemberData<'db> { - fn normalized(&self, db: &'db dyn Db) -> Self { - self.normalized_impl(db, &NormalizedVisitor::default()) - } - - fn normalized_impl(&self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self { - kind: self.kind.normalized_impl(db, visitor), - qualifiers: self.qualifiers, - definition: None, - } - } - fn recursive_type_normalized_impl( &self, db: &'db dyn Db, @@ -627,20 +600,6 @@ enum ProtocolMemberKind<'db> { } impl<'db> ProtocolMemberKind<'db> { - fn normalized_impl(&self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - ProtocolMemberKind::Method(callable) => { - ProtocolMemberKind::Method(callable.normalized_impl(db, visitor)) - } - ProtocolMemberKind::Property(property) => { - ProtocolMemberKind::Property(property.normalized_impl(db, visitor)) - } - ProtocolMemberKind::Other(ty) => { - ProtocolMemberKind::Other(ty.normalized_impl(db, visitor)) - } - } - } - fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index fe4f96ed88df8..9edc5a27ed548 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -89,8 +89,10 @@ pub(crate) enum TypeRelation<'db> { /// The "redundancy" relation. /// - /// The redundancy relation dictates whether the union `A | B` can be safely simplified - /// to the type `A` without downstream consequences on ty's inference of types elsewhere. + /// The redundancy relation is really an alternative, less strict, version of subtyping. + /// Unlike the subtyping relation, the redundancy relation sometimes allows a non-fully-static + /// type to be considered redundant with another type, and allows some types to be considered + /// redundant with non-fully-static types. /// /// For a pair of [fully static] types `A` and `B`, the redundancy relation between `A` /// and `B` is the same as the subtyping relation. @@ -102,19 +104,28 @@ pub(crate) enum TypeRelation<'db> { /// of `C | D` is equivalent to the bottom materialization of `C`. /// More concisely: `D <: C` iff `Top[C | D] == Top[C]` AND `Bottom[C | D] == Bottom[C]`. /// - /// Practically speaking, in most respects the redundancy relation is the same as the subtyping + /// As stated above, in most respects the redundancy relation is the same as the subtyping /// relation. It is redundant to add `bool` to a union that includes `int`, because `bool` is a /// subtype of `int`, so inference of attribute access or binary expressions on the union /// `int | bool` would always produce a type that represents the same set of possible sets of /// runtime values as if ty had inferred the attribute access or binary expression on `int` /// alone. /// - /// Where the redundancy relation differs from the subtyping relation is that there are a - /// number of simplifications that can be made when simplifying unions that are not - /// strictly permitted by the subtyping relation. For example, it is safe to avoid adding - /// `Any` to a union that already includes `Any`, because `Any` already represents an - /// unknown set of possible sets of runtime values that can materialize to any type in a - /// gradual, permissive way. Inferring attribute access or binary expressions over + /// The redundancy relation is used prominently in two places as of 2026-02-25: for + /// simplifying unions and intersections in our smart type builders, and for calculating + /// equivalence between types. Union simplification is pragmatic, and passes `pure: false`; + /// equivalence checking requires "pure redundancy", and thus passes `pure: true`. Practically, + /// the behaviour difference here is that we want `Literal[False]` to always be considered + /// equivalent to `Literal[False]`, but we don't *necessarily* want `Literal[False]` to always + /// be considered redundant with `Literal[False]` if one `Literal[False]` is promotable and the + /// other is not. + /// + /// In comparing the redundancy relation with subtyping, one practical way in which they differ is + /// that the redundancy relation permits a number of simplifications that can be made when + /// simplifying unions that would not be strictly permitted by the subtyping relation. For example, + /// it is safe to avoid adding `Any` to a union that already includes `Any`, because `Any` already + /// represents an unknown set of possible sets of runtime values that can materialize to any type in + /// a gradual, permissive way. Inferring attribute access or binary expressions over /// `Any | Any` could never conceivably yield a type that represents a different set of /// possible sets of runtime values to inferring the same expression over `Any` alone; /// although `Any` is not a subtype of `Any`, top materialization of both `Any` and @@ -141,7 +152,7 @@ pub(crate) enum TypeRelation<'db> { /// /// [fully static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type /// [materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize - Redundancy, + Redundancy { pure: bool }, /// The "constraint implication" relationship, aka "implies subtype of". /// @@ -199,15 +210,11 @@ impl TypeRelation<'_> { matches!(self, TypeRelation::Subtyping) } - pub(crate) const fn is_redundancy(self) -> bool { - matches!(self, TypeRelation::Redundancy) - } - pub(crate) const fn can_safely_assume_reflexivity(self, ty: Type) -> bool { match self { TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability - | TypeRelation::Redundancy => true, + | TypeRelation::Redundancy { .. } => true, TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => { ty.subtyping_is_always_reflexive() } @@ -305,7 +312,12 @@ impl<'db> Type<'db> { other: Type<'db>, ) -> bool { self_ty - .has_relation_to(db, other, InferableTypeVars::None, TypeRelation::Redundancy) + .has_relation_to( + db, + other, + InferableTypeVars::None, + TypeRelation::Redundancy { pure: false }, + ) .is_always_satisfied(db) } @@ -461,7 +473,7 @@ impl<'db> Type<'db> { ConstraintSet::from(match relation { TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true, - TypeRelation::Redundancy => match target { + TypeRelation::Redundancy { .. } => match target { Type::Dynamic(_) => true, Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic), _ => false, @@ -471,7 +483,7 @@ impl<'db> Type<'db> { (_, Type::Dynamic(_)) => ConstraintSet::from(match relation { TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true, - TypeRelation::Redundancy => match self { + TypeRelation::Redundancy { .. } => match self { Type::Dynamic(_) => true, Type::Intersection(intersection) => { // If a `Divergent` type is involved, it must not be eliminated. @@ -810,7 +822,7 @@ impl<'db> Type<'db> { // of redundancy may not generally lead to simpler types in many situations. let self_ty = match relation { TypeRelation::Subtyping - | TypeRelation::Redundancy + | TypeRelation::Redundancy { .. } | TypeRelation::SubtypingAssuming(_) => self, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { self.bottom_materialization(db) @@ -819,7 +831,7 @@ impl<'db> Type<'db> { intersection.negative(db).iter().when_all(db, |&neg_ty| { let neg_ty = match relation { TypeRelation::Subtyping - | TypeRelation::Redundancy + | TypeRelation::Redundancy { .. } | TypeRelation::SubtypingAssuming(_) => neg_ty, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { @@ -925,7 +937,16 @@ impl<'db> Type<'db> { (left, Type::AlwaysTruthy) => ConstraintSet::from(left.bool(db).is_always_true()), // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { - target.when_equivalent_to(db, Type::object(), inferable) + relation_visitor.visit((self, target, relation), || { + Type::object().has_relation_to_impl( + db, + target, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) } // These clauses handle type variants that include function literals. A function @@ -972,7 +993,9 @@ impl<'db> Type<'db> { // For union simplification, we want to preserve the unpromotable form of a literal value, // and so redundancy is not symmetric. - (Type::LiteralValue(this), Type::LiteralValue(target)) if relation.is_redundancy() => { + (Type::LiteralValue(this), Type::LiteralValue(target)) + if matches!(relation, TypeRelation::Redundancy { pure: false }) => + { ConstraintSet::from(this.kind() == target.kind() && this.is_promotable()) } @@ -1328,8 +1351,8 @@ impl<'db> Type<'db> { (Type::Callable(_), _) => ConstraintSet::from(false), - (Type::BoundSuper(_), Type::BoundSuper(_)) => { - self.when_equivalent_to(db, target, inferable) + (Type::BoundSuper(left), Type::BoundSuper(right)) => { + left.is_equivalent_to_impl(db, right, relation_visitor, disjointness_visitor) } (Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).has_relation_to_impl( db, @@ -1576,126 +1599,44 @@ impl<'db> Type<'db> { /// /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { - self.when_equivalent_to(db, other, InferableTypeVars::None) - .is_always_satisfied(db) + self.when_equivalent_to(db, other).is_always_satisfied(db) } pub(crate) fn when_equivalent_to( self, db: &'db dyn Db, other: Type<'db>, - inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db> { - self.is_equivalent_to_impl(db, other, inferable, &IsEquivalentVisitor::default()) + let relation_visitor = HasRelationToVisitor::default(); + let disjointness_visitor = IsDisjointVisitor::default(); + self.when_equivalent_to_impl(db, other, &relation_visitor, &disjointness_visitor) } - pub(crate) fn is_equivalent_to_impl( + pub(crate) fn when_equivalent_to_impl( self, db: &'db dyn Db, other: Type<'db>, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, + relation_visitor: &HasRelationToVisitor<'db>, + disjointness_visitor: &IsDisjointVisitor<'db>, ) -> ConstraintSet<'db> { - if self == other { - return ConstraintSet::from(true); - } - - match (self, other) { - // The `Divergent` type is a special type that is not equivalent to other kinds of dynamic types, - // which prevents `Divergent` from being eliminated during union reduction. - (Type::Dynamic(_), Type::Dynamic(DynamicType::Divergent(_))) - | (Type::Dynamic(DynamicType::Divergent(_)), Type::Dynamic(_)) => { - ConstraintSet::from(false) - } - (Type::Dynamic(_), Type::Dynamic(_)) => ConstraintSet::from(true), - - (Type::SubclassOf(first), Type::SubclassOf(second)) => { - match (first.subclass_of(), second.subclass_of()) { - (first, second) if first == second => ConstraintSet::from(true), - (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from(true) - } - _ => ConstraintSet::from(false), - } - } - - (Type::TypeAlias(self_alias), _) => { - let self_alias_ty = self_alias.value_type(db).normalized(db); - visitor.visit((self_alias_ty, other), || { - self_alias_ty.is_equivalent_to_impl(db, other, inferable, visitor) - }) - } - - (_, Type::TypeAlias(other_alias)) => { - let other_alias_ty = other_alias.value_type(db).normalized(db); - visitor.visit((self, other_alias_ty), || { - self.is_equivalent_to_impl(db, other_alias_ty, inferable, visitor) - }) - } - - (Type::NewTypeInstance(self_newtype), Type::NewTypeInstance(other_newtype)) => { - ConstraintSet::from(self_newtype.is_equivalent_to_impl(db, other_newtype)) - } - - (Type::NominalInstance(first), Type::NominalInstance(second)) => { - first.is_equivalent_to_impl(db, second, inferable, visitor) - } - - (Type::Union(first), Type::Union(second)) => { - first.is_equivalent_to_impl(db, second, inferable, visitor) - } - - (Type::Intersection(first), Type::Intersection(second)) => { - first.is_equivalent_to_impl(db, second, inferable, visitor) - } - - (Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => { - self_function.is_equivalent_to_impl(db, target_function, inferable, visitor) - } - (Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => { - self_method.is_equivalent_to_impl(db, target_method, inferable, visitor) - } - (Type::KnownBoundMethod(self_method), Type::KnownBoundMethod(target_method)) => { - self_method.is_equivalent_to_impl(db, target_method, inferable, visitor) - } - (Type::Callable(first), Type::Callable(second)) => { - first.is_equivalent_to_impl(db, second, inferable, visitor) - } - - (Type::LiteralValue(left), Type::LiteralValue(right)) => { - ConstraintSet::from(left.kind() == right.kind()) - } - - (Type::ProtocolInstance(first), Type::ProtocolInstance(second)) => { - first.is_equivalent_to_impl(db, second, inferable, visitor) - } - (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) - | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { - ConstraintSet::from(n.is_object() && protocol.normalized(db) == nominal) - } - // An instance of an enum class is equivalent to an enum literal of that class, - // if that enum has only has one member. - (Type::NominalInstance(instance), Type::LiteralValue(literal)) - | (Type::LiteralValue(literal), Type::NominalInstance(instance)) - if literal.is_enum() => - { - let literal = literal.as_enum().unwrap(); - if literal.enum_class_instance(db) != Type::NominalInstance(instance) { - return ConstraintSet::from(false); - } - ConstraintSet::from(is_single_member_enum(db, instance.class_literal(db))) - } - - (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { - left.is_equivalent_to_impl(db, right, inferable, visitor) - } - - (Type::TypedDict(left), Type::TypedDict(right)) => visitor.visit((self, other), || { - left.is_equivalent_to_impl(db, right, inferable, visitor) - }), - - _ => ConstraintSet::from(false), - } + self.has_relation_to_impl( + db, + other, + InferableTypeVars::None, + TypeRelation::Redundancy { pure: true }, + relation_visitor, + disjointness_visitor, + ) + .and(db, || { + other.has_relation_to_impl( + db, + self, + InferableTypeVars::None, + TypeRelation::Redundancy { pure: true }, + relation_visitor, + disjointness_visitor, + ) + }) } /// Return true if `self & other` should simplify to `Never`: @@ -2468,9 +2409,9 @@ impl<'db> Type<'db> { ) } - (Type::BoundSuper(_), Type::BoundSuper(_)) => { - self.when_equivalent_to(db, other, inferable).negate(db) - } + (Type::BoundSuper(left), Type::BoundSuper(right)) => left + .is_equivalent_to_impl(db, right, relation_visitor, disjointness_visitor) + .negate(db), (Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => { KnownClass::Super.to_instance(db).is_disjoint_from_impl( db, diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index e837a850a7568..5ece2a84f45e4 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -21,14 +21,11 @@ use crate::semantic_index::definition::Definition; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::generics::{GenericContext, InferableTypeVars, walk_generic_context}; use crate::types::infer::infer_deferred_types; -use crate::types::relation::{ - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, -}; +use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypeKind, - FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, NormalizedVisitor, - ParamSpecAttrKind, SelfBinding, TypeContext, TypeMapping, VarianceInferable, - infer_complete_scope_types, todo_type, + FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, ParamSpecAttrKind, SelfBinding, + TypeContext, TypeMapping, VarianceInferable, infer_complete_scope_types, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -106,18 +103,6 @@ impl<'db> CallableSignature<'db> { })) } - pub(crate) fn normalized_impl( - &self, - db: &'db dyn Db, - visitor: &NormalizedVisitor<'db>, - ) -> Self { - Self::from_overloads( - self.overloads - .iter() - .map(|signature| signature.normalized_impl(db, visitor)), - ) - } - pub(super) fn recursive_type_normalized_impl( &self, db: &'db dyn Db, @@ -300,22 +285,6 @@ impl<'db> CallableSignature<'db> { } } - fn is_subtype_of_impl( - &self, - db: &'db dyn Db, - other: &Self, - inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { - self.has_relation_to_impl( - db, - other, - inferable, - TypeRelation::Subtyping, - &HasRelationToVisitor::default(), - &IsDisjointVisitor::default(), - ) - } - pub(crate) fn has_relation_to_impl( &self, db: &'db dyn Db, @@ -543,32 +512,6 @@ impl<'db> CallableSignature<'db> { }), } } - - /// Check whether this callable type is equivalent to another callable type. - /// - /// See [`Type::is_equivalent_to`] for more details. - pub(crate) fn is_equivalent_to_impl( - &self, - db: &'db dyn Db, - other: &Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - match (self.overloads.as_slice(), other.overloads.as_slice()) { - ([self_signature], [other_signature]) => { - // Common case: both callable types contain a single signature, use the custom - // equivalence check instead of delegating it to the subtype check. - self_signature.is_equivalent_to_impl(db, other_signature, inferable, visitor) - } - (_, _) => { - if self == other { - return ConstraintSet::from(true); - } - self.is_subtype_of_impl(db, other, inferable) - .and(db, || other.is_subtype_of_impl(db, self, inferable)) - } - } - } } impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { @@ -766,28 +709,6 @@ impl<'db> Signature<'db> { self } - pub(crate) fn normalized_impl( - &self, - db: &'db dyn Db, - visitor: &NormalizedVisitor<'db>, - ) -> Self { - Self { - generic_context: self - .generic_context - .map(|ctx| ctx.normalized_impl(db, visitor)), - // Discard the definition when normalizing, so that two equivalent signatures - // with different `Definition`s share the same Salsa ID when normalized - definition: None, - parameters: Parameters::new( - db, - self.parameters - .iter() - .map(|param| param.normalized_impl(db, visitor)), - ), - return_ty: self.return_ty.normalized_impl(db, visitor), - } - } - pub(super) fn recursive_type_normalized_impl( &self, db: &'db dyn Db, @@ -984,128 +905,6 @@ impl<'db> Signature<'db> { } } - /// Return `true` if `self` has exactly the same set of possible static materializations as - /// `other` (if `self` represents the same set of possible sets of possible runtime objects as - /// `other`). - pub(crate) fn is_equivalent_to_impl( - &self, - db: &'db dyn Db, - other: &Signature<'db>, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - // If either signature is generic, their typevars should also be considered inferable when - // checking whether the signatures are equivalent, since we only need to find one - // specialization that causes the check to succeed. - // - // TODO: We should alpha-rename these typevars, too, to correctly handle when a generic - // callable refers to typevars from within the context that defines them. This primarily - // comes up when referring to a generic function recursively from within its body: - // - // def identity[T](t: T) -> T: - // # Here, TypeOf[identity2] is a generic callable that should consider T to be - // # inferable, even though other uses of T in the function body are non-inferable. - // return t - let self_inferable = self.inferable_typevars(db); - let other_inferable = other.inferable_typevars(db); - let inferable = inferable.merge(&self_inferable); - let inferable = inferable.merge(&other_inferable); - - // `inner` will create a constraint set that references these newly inferable typevars. - let when = self.is_equivalent_to_inner(db, other, inferable, visitor); - - // But the caller does not need to consider those extra typevars. Whatever constraint set - // we produce, we reduce it back down to the inferable set that the caller asked about. - // If we introduced new inferable typevars, those will be existentially quantified away - // before returning. - when.reduce_inferable(db, self_inferable.iter().chain(other_inferable.iter())) - } - - fn is_equivalent_to_inner( - &self, - db: &'db dyn Db, - other: &Signature<'db>, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - let mut result = ConstraintSet::from(true); - - if self.parameters.is_gradual() != other.parameters.is_gradual() { - return ConstraintSet::from(false); - } - - if self.parameters.len() != other.parameters.len() { - return ConstraintSet::from(false); - } - - let mut check_types = |self_type: Type<'db>, other_type: Type<'db>| { - !result - .intersect( - db, - self_type.is_equivalent_to_impl(db, other_type, inferable, visitor), - ) - .is_never_satisfied(db) - }; - - if !check_types(self.return_ty, other.return_ty) { - return result; - } - - for (self_parameter, other_parameter) in self.parameters.iter().zip(&other.parameters) { - match (self_parameter.kind(), other_parameter.kind()) { - ( - ParameterKind::PositionalOnly { - default_type: self_default, - .. - }, - ParameterKind::PositionalOnly { - default_type: other_default, - .. - }, - ) if self_default.is_some() == other_default.is_some() => {} - - ( - ParameterKind::PositionalOrKeyword { - name: self_name, - default_type: self_default, - }, - ParameterKind::PositionalOrKeyword { - name: other_name, - default_type: other_default, - }, - ) if self_default.is_some() == other_default.is_some() - && self_name == other_name => {} - - (ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => {} - - ( - ParameterKind::KeywordOnly { - name: self_name, - default_type: self_default, - }, - ParameterKind::KeywordOnly { - name: other_name, - default_type: other_default, - }, - ) if self_default.is_some() == other_default.is_some() - && self_name == other_name => {} - - (ParameterKind::KeywordVariadic { .. }, ParameterKind::KeywordVariadic { .. }) => {} - - _ => return ConstraintSet::from(false), - } - - if !check_types( - self_parameter.annotated_type(), - other_parameter.annotated_type(), - ) { - return result; - } - } - - result - } - pub(crate) fn when_constraint_set_assignable_to_signatures( &self, db: &'db dyn Db, @@ -1326,6 +1125,7 @@ impl<'db> Signature<'db> { // A gradual parameter list is a supertype of the "bottom" parameter list (*args: object, // **kwargs: object). if other.parameters.is_gradual() + && !self.parameters.is_top() && self .parameters .variadic() @@ -1349,13 +1149,18 @@ impl<'db> Signature<'db> { // If either of the parameter lists is gradual (`...`), then it is assignable to and from // any other parameter list, but not a subtype or supertype of any other parameter list. if self.parameters.is_gradual() || other.parameters.is_gradual() { - result.intersect( - db, - ConstraintSet::from( - relation.is_assignability() || relation.is_constraint_set_assignability(), + return match relation { + TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => { + ConstraintSet::from(false) + } + TypeRelation::Redundancy { .. } => result.intersect( + db, + ConstraintSet::from( + self.parameters.is_gradual() && other.parameters.is_gradual(), + ), ), - ); - return result; + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => result, + }; } if relation.is_constraint_set_assignability() { @@ -2374,67 +2179,6 @@ impl<'db> Parameter<'db> { } } - /// Strip information from the parameter so that two equivalent parameters compare equal. - /// Normalize nested unions and intersections in the annotated type. - /// - /// See [`Type::normalized`] for more details. - pub(crate) fn normalized_impl( - &self, - db: &'db dyn Db, - visitor: &NormalizedVisitor<'db>, - ) -> Self { - let Parameter { - annotated_type, - kind, - form, - has_starred_annotation, - inferred_annotation: _, - } = self; - - // Ensure unions and intersections are ordered in the annotated type. - // Unknown normalizes to Any. - let annotated_type = annotated_type.normalized_impl(db, visitor); - - // Ensure that parameter names are stripped from positional-only, variadic and keyword-variadic parameters. - // Ensure that we only record whether a parameter *has* a default - // (strip the precise *type* of the default from the parameter, replacing it with `Never`). - let kind = match kind { - ParameterKind::PositionalOnly { - name: _, - default_type, - } => ParameterKind::PositionalOnly { - name: None, - default_type: default_type.map(|_| Type::Never), - }, - ParameterKind::PositionalOrKeyword { name, default_type } => { - ParameterKind::PositionalOrKeyword { - name: name.clone(), - default_type: default_type.map(|_| Type::Never), - } - } - ParameterKind::KeywordOnly { name, default_type } => ParameterKind::KeywordOnly { - name: name.clone(), - default_type: default_type.map(|_| Type::Never), - }, - ParameterKind::Variadic { name: _ } => ParameterKind::Variadic { - name: Name::new_static("args"), - }, - ParameterKind::KeywordVariadic { name: _ } => ParameterKind::KeywordVariadic { - name: Name::new_static("kwargs"), - }, - }; - - Self { - annotated_type, - // Normalize `inferred_annotation` to `false` since it's a display-only field - // that doesn't affect type semantics. - inferred_annotation: false, - has_starred_annotation: *has_starred_annotation, - kind, - form: *form, - } - } - pub(super) fn recursive_type_normalized_impl( &self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 90e84355e0ee3..29ccdf0b97a3c 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -8,8 +8,8 @@ use crate::types::variance::VarianceInferable; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, ClassType, DynamicClassLiteral, DynamicType, FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, - NormalizedVisitor, SpecialFormType, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, - TypeVarVariance, TypedDictType, UnionType, todo_type, + SpecialFormType, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, TypeVarVariance, + TypedDictType, UnionType, todo_type, }; use crate::{Db, FxOrderSet}; @@ -274,12 +274,6 @@ impl<'db> SubclassOfType<'db> { } } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self { - subclass_of: self.subclass_of.normalized_impl(db, visitor), - } - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -469,16 +463,6 @@ impl<'db> SubclassOfInner<'db> { Self::TypeVar(bound_typevar) } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)), - Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), - Self::TypeVar(bound_typevar) => { - Self::TypeVar(bound_typevar.normalized_impl(db, visitor)) - } - } - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 8cacff51d213e..457e3179b2841 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -28,12 +28,10 @@ use crate::types::builder::RecursivelyDefined; use crate::types::class::{ClassType, KnownClass}; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::generics::InferableTypeVars; -use crate::types::relation::{ - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, -}; +use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, IntersectionType, - NormalizedVisitor, Type, TypeMapping, UnionBuilder, UnionType, + Type, TypeMapping, UnionBuilder, UnionType, }; use crate::types::{Truthiness, TypeContext}; use crate::{Db, FxOrderSet, Program}; @@ -223,18 +221,6 @@ impl<'db> TupleType<'db> { }) } - /// Return a normalized version of `self`. - /// - /// See [`Type::normalized`] for more details. - #[must_use] - pub(crate) fn normalized_impl( - self, - db: &'db dyn Db, - visitor: &NormalizedVisitor<'db>, - ) -> Option { - TupleType::new(db, &self.tuple(db).normalized_impl(db, visitor)) - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -310,17 +296,6 @@ impl<'db> TupleType<'db> { ) } - pub(crate) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - self.tuple(db) - .is_equivalent_to_impl(db, other.tuple(db), inferable, visitor) - } - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { self.tuple(db).is_single_valued(db) } @@ -427,11 +402,6 @@ impl<'db> FixedLengthTuple> { } } - #[must_use] - fn normalized_impl(&self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self::from_elements(self.0.iter().map(|ty| ty.normalized_impl(db, visitor))) - } - fn recursive_type_normalized_impl( &self, db: &'db dyn Db, @@ -592,22 +562,6 @@ impl<'db> FixedLengthTuple> { } } - fn is_equivalent_to_impl( - &self, - db: &'db dyn Db, - other: &Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - ConstraintSet::from(self.0.len() == other.0.len()).and(db, || { - (self.0.iter()) - .zip(&other.0) - .when_all(db, |(self_ty, other_ty)| { - self_ty.is_equivalent_to_impl(db, *other_ty, inferable, visitor) - }) - }) - } - fn is_single_valued(&self, db: &'db dyn Db) -> bool { self.0.iter().all(|ty| ty.is_single_valued(db)) } @@ -924,18 +878,6 @@ impl<'db> VariableLengthTuple> { } } - #[must_use] - fn normalized_impl(&self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> TupleSpec<'db> { - let prefix = self - .prenormalized_prefix_elements(db, None) - .map(|ty| ty.normalized_impl(db, visitor)); - let suffix = self - .prenormalized_suffix_elements(db, None) - .map(|ty| ty.normalized_impl(db, visitor)); - let variable = self.variable().normalized_impl(db, visitor); - TupleSpec::Variable(Self::new(prefix, variable, suffix)) - } - fn recursive_type_normalized_impl( &self, db: &'db dyn Db, @@ -1220,41 +1162,6 @@ impl<'db> VariableLengthTuple> { } } } - - fn is_equivalent_to_impl( - &self, - db: &'db dyn Db, - other: &Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - self.variable() - .is_equivalent_to_impl(db, other.variable(), inferable, visitor) - .and(db, || { - self.prenormalized_prefix_elements(db, None) - .zip_longest(other.prenormalized_prefix_elements(db, None)) - .when_all(db, |pair| match pair { - EitherOrBoth::Both(self_ty, other_ty) => { - self_ty.is_equivalent_to_impl(db, other_ty, inferable, visitor) - } - EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => { - ConstraintSet::from(false) - } - }) - }) - .and(db, || { - self.prenormalized_suffix_elements(db, None) - .zip_longest(other.prenormalized_suffix_elements(db, None)) - .when_all(db, |pair| match pair { - EitherOrBoth::Both(self_ty, other_ty) => { - self_ty.is_equivalent_to_impl(db, other_ty, inferable, visitor) - } - EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => { - ConstraintSet::from(false) - } - }) - }) - } } impl<'db> PyIndex<'db> for &VariableLengthTuple> { @@ -1419,17 +1326,6 @@ impl<'db> Tuple> { } } - pub(crate) fn normalized_impl( - &self, - db: &'db dyn Db, - visitor: &NormalizedVisitor<'db>, - ) -> Self { - match self { - Tuple::Fixed(tuple) => Tuple::Fixed(tuple.normalized_impl(db, visitor)), - Tuple::Variable(tuple) => tuple.normalized_impl(db, visitor), - } - } - pub(super) fn recursive_type_normalized_impl( &self, db: &'db dyn Db, @@ -1507,26 +1403,6 @@ impl<'db> Tuple> { } } - fn is_equivalent_to_impl( - &self, - db: &'db dyn Db, - other: &Self, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - match (self, other) { - (Tuple::Fixed(self_tuple), Tuple::Fixed(other_tuple)) => { - self_tuple.is_equivalent_to_impl(db, other_tuple, inferable, visitor) - } - (Tuple::Variable(self_tuple), Tuple::Variable(other_tuple)) => { - self_tuple.is_equivalent_to_impl(db, other_tuple, inferable, visitor) - } - (Tuple::Fixed(_), Tuple::Variable(_)) | (Tuple::Variable(_), Tuple::Fixed(_)) => { - ConstraintSet::from(false) - } - } - } - pub(super) fn is_disjoint_from_impl( &self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs deleted file mode 100644 index 2c9c10a5f53ec..0000000000000 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ /dev/null @@ -1,357 +0,0 @@ -use std::cmp::Ordering; - -use salsa::plumbing::AsId; - -use crate::{ - db::Db, - types::{LiteralValueTypeKind, bound_super::SuperOwnerKind}, -}; - -use super::{ - DynamicType, TodoType, Type, TypeGuardLike, TypeGuardType, TypeIsType, class_base::ClassBase, - subclass_of::SubclassOfInner, -}; - -/// Return an [`Ordering`] that describes the canonical order in which two types should appear -/// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them -/// to be compared for equivalence. -/// -/// Two intersections are compared lexicographically. Element types in the intersection must -/// already be sorted. Two unions are never compared in this function because DNF does not permit -/// nested unions. -/// -/// ## Why not just implement [`Ord`] on [`Type`]? -/// -/// It would be fairly easy to slap `#[derive(PartialOrd, Ord)]` on [`Type`], and the ordering we -/// create here is not user-facing. However, it doesn't really "make sense" for `Type` to implement -/// [`Ord`] in terms of the semantics. There are many different ways in which you could plausibly -/// sort a list of types; this is only one (somewhat arbitrary, at times) possible ordering. -pub(super) fn union_or_intersection_elements_ordering<'db>( - db: &'db dyn Db, - left: &Type<'db>, - right: &Type<'db>, -) -> Ordering { - debug_assert_eq!( - *left, - left.normalized(db), - "`left` must be normalized before a meaningful ordering can be established" - ); - debug_assert_eq!( - *right, - right.normalized(db), - "`right` must be normalized before a meaningful ordering can be established" - ); - - if left == right { - return Ordering::Equal; - } - - match (left, right) { - (Type::Never, _) => Ordering::Less, - (_, Type::Never) => Ordering::Greater, - - (Type::LiteralValue(left), Type::LiteralValue(right)) => { - match (left.kind(), right.kind()) { - (LiteralValueTypeKind::LiteralString, _) => Ordering::Less, - (_, LiteralValueTypeKind::LiteralString) => Ordering::Greater, - - (LiteralValueTypeKind::Bool(left), LiteralValueTypeKind::Bool(right)) => { - left.cmp(&right) - } - (LiteralValueTypeKind::Bool(_), _) => Ordering::Less, - (_, LiteralValueTypeKind::Bool(_)) => Ordering::Greater, - - (LiteralValueTypeKind::Int(left), LiteralValueTypeKind::Int(right)) => { - left.cmp(&right) - } - (LiteralValueTypeKind::Int(_), _) => Ordering::Less, - (_, LiteralValueTypeKind::Int(_)) => Ordering::Greater, - - (LiteralValueTypeKind::String(left), LiteralValueTypeKind::String(right)) => { - left.cmp(&right) - } - (LiteralValueTypeKind::String(_), _) => Ordering::Less, - (_, LiteralValueTypeKind::String(_)) => Ordering::Greater, - - (LiteralValueTypeKind::Bytes(left), LiteralValueTypeKind::Bytes(right)) => { - left.cmp(&right) - } - (LiteralValueTypeKind::Bytes(_), _) => Ordering::Less, - (_, LiteralValueTypeKind::Bytes(_)) => Ordering::Greater, - - (LiteralValueTypeKind::Enum(left), LiteralValueTypeKind::Enum(right)) => { - left.cmp(&right) - } - } - } - - (Type::LiteralValue(_), _) => Ordering::Less, - (_, Type::LiteralValue(_)) => Ordering::Greater, - - (Type::FunctionLiteral(left), Type::FunctionLiteral(right)) => left.cmp(right), - (Type::FunctionLiteral(_), _) => Ordering::Less, - (_, Type::FunctionLiteral(_)) => Ordering::Greater, - - (Type::BoundMethod(left), Type::BoundMethod(right)) => left.cmp(right), - (Type::BoundMethod(_), _) => Ordering::Less, - (_, Type::BoundMethod(_)) => Ordering::Greater, - - (Type::KnownBoundMethod(left), Type::KnownBoundMethod(right)) => left.cmp(right), - (Type::KnownBoundMethod(_), _) => Ordering::Less, - (_, Type::KnownBoundMethod(_)) => Ordering::Greater, - - (Type::WrapperDescriptor(left), Type::WrapperDescriptor(right)) => left.cmp(right), - (Type::WrapperDescriptor(_), _) => Ordering::Less, - (_, Type::WrapperDescriptor(_)) => Ordering::Greater, - - (Type::DataclassDecorator(left), Type::DataclassDecorator(right)) => left.cmp(right), - (Type::DataclassDecorator(_), _) => Ordering::Less, - (_, Type::DataclassDecorator(_)) => Ordering::Greater, - - (Type::DataclassTransformer(left), Type::DataclassTransformer(right)) => left.cmp(right), - (Type::DataclassTransformer(_), _) => Ordering::Less, - (_, Type::DataclassTransformer(_)) => Ordering::Greater, - - (Type::Callable(left), Type::Callable(right)) => left.cmp(right), - (Type::Callable(_), _) => Ordering::Less, - (_, Type::Callable(_)) => Ordering::Greater, - - (Type::ModuleLiteral(left), Type::ModuleLiteral(right)) => left.cmp(right), - (Type::ModuleLiteral(_), _) => Ordering::Less, - (_, Type::ModuleLiteral(_)) => Ordering::Greater, - - (Type::ClassLiteral(left), Type::ClassLiteral(right)) => left.cmp(right), - (Type::ClassLiteral(_), _) => Ordering::Less, - (_, Type::ClassLiteral(_)) => Ordering::Greater, - - (Type::GenericAlias(left), Type::GenericAlias(right)) => left.cmp(right), - (Type::GenericAlias(_), _) => Ordering::Less, - (_, Type::GenericAlias(_)) => Ordering::Greater, - - (Type::SubclassOf(left), Type::SubclassOf(right)) => { - match (left.subclass_of(), right.subclass_of()) { - (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => left.cmp(&right), - (SubclassOfInner::Class(_), _) => Ordering::Less, - (_, SubclassOfInner::Class(_)) => Ordering::Greater, - (SubclassOfInner::Dynamic(left), SubclassOfInner::Dynamic(right)) => { - dynamic_elements_ordering(left, right) - } - (SubclassOfInner::TypeVar(left), SubclassOfInner::TypeVar(right)) => { - left.as_id().cmp(&right.as_id()) - } - (SubclassOfInner::TypeVar(_), _) => Ordering::Less, - (_, SubclassOfInner::TypeVar(_)) => Ordering::Greater, - } - } - - (Type::SubclassOf(_), _) => Ordering::Less, - (_, Type::SubclassOf(_)) => Ordering::Greater, - - (Type::TypeIs(left), Type::TypeIs(right)) => typeis_ordering(db, *left, *right), - (Type::TypeIs(_), _) => Ordering::Less, - (_, Type::TypeIs(_)) => Ordering::Greater, - - (Type::TypeGuard(left), Type::TypeGuard(right)) => typeguard_ordering(db, *left, *right), - (Type::TypeGuard(_), _) => Ordering::Less, - (_, Type::TypeGuard(_)) => Ordering::Greater, - - (Type::NominalInstance(left), Type::NominalInstance(right)) => { - left.class(db).cmp(&right.class(db)) - } - (Type::NominalInstance(_), _) => Ordering::Less, - (_, Type::NominalInstance(_)) => Ordering::Greater, - - (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => { - left_proto.cmp(right_proto) - } - (Type::ProtocolInstance(_), _) => Ordering::Less, - (_, Type::ProtocolInstance(_)) => Ordering::Greater, - - // This is one place where we want to compare the typevar identities directly, instead of - // falling back on `is_same_typevar_as` or `can_be_bound_for`. - (Type::TypeVar(left), Type::TypeVar(right)) => left.as_id().cmp(&right.as_id()), - (Type::TypeVar(_), _) => Ordering::Less, - (_, Type::TypeVar(_)) => Ordering::Greater, - - (Type::AlwaysTruthy, _) => Ordering::Less, - (_, Type::AlwaysTruthy) => Ordering::Greater, - - (Type::AlwaysFalsy, _) => Ordering::Less, - (_, Type::AlwaysFalsy) => Ordering::Greater, - - (Type::BoundSuper(left), Type::BoundSuper(right)) => { - (match (left.pivot_class(db), right.pivot_class(db)) { - (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right), - (ClassBase::Class(_), _) => Ordering::Less, - (_, ClassBase::Class(_)) => Ordering::Greater, - - (ClassBase::Protocol, _) => Ordering::Less, - (_, ClassBase::Protocol) => Ordering::Greater, - - (ClassBase::Generic, _) => Ordering::Less, - (_, ClassBase::Generic) => Ordering::Greater, - - (ClassBase::TypedDict, _) => Ordering::Less, - (_, ClassBase::TypedDict) => Ordering::Greater, - - (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { - dynamic_elements_ordering(left, right) - } - }) - .then_with(|| match (left.owner(db), right.owner(db)) { - (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => left.cmp(&right), - (SuperOwnerKind::Class(_), _) => Ordering::Less, - (_, SuperOwnerKind::Class(_)) => Ordering::Greater, - (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { - left.class(db).cmp(&right.class(db)) - } - (SuperOwnerKind::Instance(_), _) => Ordering::Less, - (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, - ( - SuperOwnerKind::InstanceTypeVar(left, _), - SuperOwnerKind::InstanceTypeVar(right, _), - ) => left.cmp(&right), - (SuperOwnerKind::InstanceTypeVar(..), _) => Ordering::Less, - (_, SuperOwnerKind::InstanceTypeVar(..)) => Ordering::Greater, - (SuperOwnerKind::ClassTypeVar(left, _), SuperOwnerKind::ClassTypeVar(right, _)) => { - left.cmp(&right) - } - (SuperOwnerKind::ClassTypeVar(..), _) => Ordering::Less, - (_, SuperOwnerKind::ClassTypeVar(..)) => Ordering::Greater, - (SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => { - dynamic_elements_ordering(left, right) - } - }) - } - (Type::BoundSuper(_), _) => Ordering::Less, - (_, Type::BoundSuper(_)) => Ordering::Greater, - - (Type::SpecialForm(left), Type::SpecialForm(right)) => left.cmp(right), - (Type::SpecialForm(_), _) => Ordering::Less, - (_, Type::SpecialForm(_)) => Ordering::Greater, - - (Type::KnownInstance(left), Type::KnownInstance(right)) => left.cmp(right), - (Type::KnownInstance(_), _) => Ordering::Less, - (_, Type::KnownInstance(_)) => Ordering::Greater, - - (Type::PropertyInstance(left), Type::PropertyInstance(right)) => left.cmp(right), - (Type::PropertyInstance(_), _) => Ordering::Less, - (_, Type::PropertyInstance(_)) => Ordering::Greater, - - (Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right), - (Type::Dynamic(_), _) => Ordering::Less, - (_, Type::Dynamic(_)) => Ordering::Greater, - - (Type::TypeAlias(left), Type::TypeAlias(right)) => left.cmp(right), - (Type::TypeAlias(_), _) => Ordering::Less, - (_, Type::TypeAlias(_)) => Ordering::Greater, - - (Type::TypedDict(left), Type::TypedDict(right)) => left.cmp(right), - (Type::TypedDict(_), _) => Ordering::Less, - (_, Type::TypedDict(_)) => Ordering::Greater, - - (Type::NewTypeInstance(left), Type::NewTypeInstance(right)) => left.cmp(right), - (Type::NewTypeInstance(_), _) => Ordering::Less, - (_, Type::NewTypeInstance(_)) => Ordering::Greater, - - (Type::Union(_), _) | (_, Type::Union(_)) => { - unreachable!("our type representation does not permit nested unions"); - } - - (Type::Intersection(left), Type::Intersection(right)) => { - // Lexicographically compare the elements of the two unequal intersections. - let left_positive = left.positive(db); - let right_positive = right.positive(db); - if left_positive.len() != right_positive.len() { - return left_positive.len().cmp(&right_positive.len()); - } - let left_negative = left.negative(db); - let right_negative = right.negative(db); - if left_negative.len() != right_negative.len() { - return left_negative.len().cmp(&right_negative.len()); - } - for (left, right) in left_positive.iter().zip(right_positive) { - let ordering = union_or_intersection_elements_ordering(db, left, right); - if ordering != Ordering::Equal { - return ordering; - } - } - for (left, right) in left_negative.iter().zip(right_negative) { - let ordering = union_or_intersection_elements_ordering(db, left, right); - if ordering != Ordering::Equal { - return ordering; - } - } - - unreachable!("Two equal, normalized intersections should share the same Salsa ID") - } - } -} - -/// Determine a canonical order for two instances of [`DynamicType`]. -fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering { - match (left, right) { - (DynamicType::Any, _) => Ordering::Less, - (_, DynamicType::Any) => Ordering::Greater, - - (DynamicType::Unknown, _) => Ordering::Less, - (_, DynamicType::Unknown) => Ordering::Greater, - - (DynamicType::UnknownGeneric(_), _) => Ordering::Less, - (_, DynamicType::UnknownGeneric(_)) => Ordering::Greater, - - (DynamicType::UnspecializedTypeVar, _) => Ordering::Less, - (_, DynamicType::UnspecializedTypeVar) => Ordering::Greater, - - #[cfg(debug_assertions)] - (DynamicType::Todo(TodoType(left)), DynamicType::Todo(TodoType(right))) => left.cmp(right), - - #[cfg(not(debug_assertions))] - (DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal, - - (DynamicType::TodoUnpack, _) => Ordering::Less, - (_, DynamicType::TodoUnpack) => Ordering::Greater, - - (DynamicType::TodoStarredExpression, _) => Ordering::Less, - (_, DynamicType::TodoStarredExpression) => Ordering::Greater, - - (DynamicType::TodoTypeVarTuple, _) => Ordering::Less, - (_, DynamicType::TodoTypeVarTuple) => Ordering::Greater, - - (DynamicType::Divergent(left), DynamicType::Divergent(right)) => left.cmp(&right), - (DynamicType::Divergent(_), _) => Ordering::Less, - (_, DynamicType::Divergent(_)) => Ordering::Greater, - } -} - -/// Generic helper for ordering type guard-like types. -/// -/// The following criteria are considered, in order: -/// * Boundness: Unbound precedes bound -/// * Symbol name: String comparison -/// * Guarded type: [`union_or_intersection_elements_ordering`] -fn guard_like_ordering<'db, T: TypeGuardLike<'db>>(db: &'db dyn Db, left: T, right: T) -> Ordering { - let (left_ty, right_ty) = (left.return_type(db), right.return_type(db)); - - match (left.place_info(db), right.place_info(db)) { - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - - (None, None) => union_or_intersection_elements_ordering(db, &left_ty, &right_ty), - - (Some(_), Some(_)) => match left.place_name(db).cmp(&right.place_name(db)) { - Ordering::Equal => union_or_intersection_elements_ordering(db, &left_ty, &right_ty), - ordering => ordering, - }, - } -} - -/// Determine a canonical order for two instances of [`TypeIsType`]. -fn typeis_ordering(db: &dyn Db, left: TypeIsType, right: TypeIsType) -> Ordering { - guard_like_ordering(db, left, right) -} - -/// Determine a canonical order for two instances of [`TypeGuardType`]. -fn typeguard_ordering(db: &dyn Db, left: TypeGuardType, right: TypeGuardType) -> Ordering { - guard_like_ordering(db, left, right) -} diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index db318b1e9bdca..f0898a856c6ba 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -19,14 +19,12 @@ use super::diagnostic::{ use super::{ApplyTypeMappingVisitor, IntersectionBuilder, Type, TypeMapping, visitor}; use crate::Db; use crate::semantic_index::definition::Definition; +use crate::types::TypeContext; use crate::types::TypeDefinition; use crate::types::class::FieldKind; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::generics::InferableTypeVars; -use crate::types::relation::{ - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, -}; -use crate::types::{NormalizedVisitor, TypeContext}; +use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; bitflags! { /// Used for `TypedDict` class parameters. @@ -315,50 +313,6 @@ impl<'db> TypedDictType<'db> { } } - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - TypedDictType::Class(_) => { - let synthesized = SynthesizedTypedDictType::new(db, self.items(db)); - TypedDictType::Synthesized(synthesized.normalized_impl(db, visitor)) - } - TypedDictType::Synthesized(synthesized) => { - TypedDictType::Synthesized(synthesized.normalized_impl(db, visitor)) - } - } - } - - pub(crate) fn is_equivalent_to_impl( - self, - db: &'db dyn Db, - other: TypedDictType<'db>, - inferable: InferableTypeVars<'_, 'db>, - visitor: &IsEquivalentVisitor<'db>, - ) -> ConstraintSet<'db> { - // TODO: `closed` and `extra_items` support will go here. Until then we don't look at the - // params at all, because `total` is already incorporated into `FieldKind`. - - // Since both sides' fields are pre-sorted into `BTreeMap`s, we can iterate over them in - // sorted order instead of paying for a lookup for each field, as long as their lengths are - // the same. - if self.items(db).len() != other.items(db).len() { - return ConstraintSet::from(false); - } - self.items(db).iter().zip(other.items(db)).when_all( - db, - |((name, field), (other_name, other_field))| { - if name != other_name || field.flags != other_field.flags { - return ConstraintSet::from(false); - } - field.declared_ty.is_equivalent_to_impl( - db, - other_field.declared_ty, - inferable, - visitor, - ) - }, - ) - } - /// Two `TypedDict`s `A` and `B` are disjoint if it's impossible to come up with a third /// `TypedDict` `C` that's fully-static and assignable to both of them. /// @@ -1102,18 +1056,6 @@ impl<'db> SynthesizedTypedDictType<'db> { SynthesizedTypedDictType::new(db, items) } - - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - let items = self - .items(db) - .iter() - .map(|(name, field)| { - let field = field.clone().normalized_impl(db, visitor); - (name.clone(), field) - }) - .collect::>(); - Self::new(db, items) - } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, get_size2::GetSize, salsa::Update)] @@ -1183,17 +1125,6 @@ impl<'db> TypedDictField<'db> { first_declaration: self.first_declaration, } } - - pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - Self { - declared_ty: self.declared_ty.normalized_impl(db, visitor), - flags: self.flags, - // A normalized typed-dict field does not hold onto the original declaration, - // since a normalized typed-dict is an abstract type where equality does not depend - // on the source-code definition. - first_declaration: None, - } - } } pub(super) struct TypedDictFieldBuilder<'db> { From 85bb0265e9c571cf4c954ce8f85624c04e36e402 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 25 Feb 2026 15:11:03 -0800 Subject: [PATCH 092/261] Try out assigned reviewers for ruff (#23571) --- .github/pr-assignee-pools.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/pr-assignee-pools.toml b/.github/pr-assignee-pools.toml index 994a250e83ef9..14a9e91e318b8 100644 --- a/.github/pr-assignee-pools.toml +++ b/.github/pr-assignee-pools.toml @@ -1,3 +1,11 @@ +[[pools]] +name = "ruff" +paths = [ + "/crates/ruff/**", + "/crates/ruff_linter/**", +] +reviewers = ["amyreese", "ntBre"] + [[pools]] name = "ty-semantic" paths = ["/crates/ty_python_semantic/**"] From db48804c76fad959ad0c4cafe891bee1299885de Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 25 Feb 2026 15:57:55 -0800 Subject: [PATCH 093/261] [`ruff`] Ignore "unknown" rule codes in `RUF100` when `RUF102` is enabled (#23531) --- crates/ruff_linter/src/checkers/noqa.rs | 5 -- .../src/rules/ruff/rules/unused_noqa.rs | 23 ++------ ..._linter__rules__ruff__tests__ruf100_0.snap | 58 +------------------ ...__rules__ruff__tests__ruf100_0_prefix.snap | 39 +------------ crates/ruff_linter/src/suppression.rs | 1 - 5 files changed, 8 insertions(+), 118 deletions(-) diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index 83f026a462402..bef2073f3c858 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -201,7 +201,6 @@ pub(crate) fn check_noqa( if !(disabled_codes.is_empty() && duplicated_codes.is_empty() - && unknown_codes.is_empty() && unmatched_codes.is_empty()) { let edit = if valid_codes.is_empty() { @@ -233,10 +232,6 @@ pub(crate) fn check_noqa( .iter() .map(|code| (*code).to_string()) .collect(), - unknown: unknown_codes - .iter() - .map(|code| (*code).to_string()) - .collect(), unmatched: unmatched_codes .iter() .map(|code| (*code).to_string()) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs index cf60acfcb6306..b5e3f3062fc75 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs @@ -8,7 +8,6 @@ use crate::AlwaysFixableViolation; pub(crate) struct UnusedCodes { pub disabled: Vec, pub duplicated: Vec, - pub unknown: Vec, pub unmatched: Vec, } @@ -71,16 +70,16 @@ impl UnusedNOQAKind { /// pass /// ``` /// -/// ## Options +/// ## See also /// -/// This rule will flag rule codes that are unknown to Ruff, even if they are -/// valid for other tools. You can tell Ruff to ignore such codes by configuring -/// the list of known "external" rule codes with the following option: -/// -/// - `lint.external` +/// This rule ignores any codes that are unknown to Ruff, as it can't determine +/// if the codes are valid or used by other tools. Enable [`invalid-rule-code`][RUF102] +/// to flag any unknown rule codes. /// /// ## References /// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression) +/// +/// [RUF102]: https://docs.astral.sh/ruff/rules/invalid-rule-code/ #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct UnusedNOQA { @@ -124,16 +123,6 @@ impl AlwaysFixableViolation for UnusedNOQA { .join(", ") )); } - if !codes.unknown.is_empty() { - codes_by_reason.push(format!( - "unknown: {}", - codes - .unknown - .iter() - .map(|code| format!("`{code}`")) - .join(", ") - )); - } if codes_by_reason.is_empty() { format!("Unused {}", self.kind.as_str()) } else { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap index 1da7089332bf7..f4cec08d34ffa 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap @@ -95,25 +95,6 @@ help: Remove unused `noqa` directive 24 | # Invalid (but external) 25 | d = 1 # noqa: V500 -RUF100 [*] Unused `noqa` directive (unknown: `V500`) - --> RUF100_0.py:25:12 - | -24 | # Invalid (but external) -25 | d = 1 # noqa: V500 - | ^^^^^^^^^^^^ -26 | -27 | # fmt: off - | -help: Remove unused `noqa` directive -22 | d = 1 # noqa: F841, V101 -23 | -24 | # Invalid (but external) - - d = 1 # noqa: V500 -25 + d = 1 -26 | -27 | # fmt: off -28 | # Invalid - no space before # - RUF100 [*] Unused `noqa` directive (unused: `E501`) --> RUF100_0.py:29:12 | @@ -272,25 +253,6 @@ help: Remove unused `noqa` directive 95 | 96 | def f(): -RUF100 [*] Unused `noqa` directive (unknown: `E50`) - --> RUF100_0.py:107:12 - | -105 | def f(): -106 | # Invalid - nonexistent error code with multibyte character -107 | d = 1 # …noqa: F841, E50 - | ^^^^^^^^^^^^^^^^^ -108 | e = 1 # …noqa: E50 - | -help: Remove unused `noqa` directive -104 | -105 | def f(): -106 | # Invalid - nonexistent error code with multibyte character - - d = 1 # …noqa: F841, E50 -107 + d = 1 # noqa: F841 -108 | e = 1 # …noqa: E50 -109 | -110 | - F841 [*] Local variable `e` is assigned to but never used --> RUF100_0.py:108:5 | @@ -309,25 +271,7 @@ help: Remove assignment to unused variable `e` 110 | def f(): note: This is an unsafe fix and may change runtime behavior -RUF100 [*] Unused `noqa` directive (unknown: `E50`) - --> RUF100_0.py:108:12 - | -106 | # Invalid - nonexistent error code with multibyte character -107 | d = 1 # …noqa: F841, E50 -108 | e = 1 # …noqa: E50 - | ^^^^^^^^^^^ - | -help: Remove unused `noqa` directive -105 | def f(): -106 | # Invalid - nonexistent error code with multibyte character -107 | d = 1 # …noqa: F841, E50 - - e = 1 # …noqa: E50 -108 + e = 1 -109 | -110 | -111 | def f(): - -RUF100 [*] Unused `noqa` directive (duplicated: `F841`; unknown: `X200`) +RUF100 [*] Unused `noqa` directive (duplicated: `F841`) --> RUF100_0.py:118:12 | 116 | # Check duplicate code detection diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap index 4ff79c6ebc8c2..f4cec08d34ffa 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap @@ -253,25 +253,6 @@ help: Remove unused `noqa` directive 95 | 96 | def f(): -RUF100 [*] Unused `noqa` directive (unknown: `E50`) - --> RUF100_0.py:107:12 - | -105 | def f(): -106 | # Invalid - nonexistent error code with multibyte character -107 | d = 1 # …noqa: F841, E50 - | ^^^^^^^^^^^^^^^^^ -108 | e = 1 # …noqa: E50 - | -help: Remove unused `noqa` directive -104 | -105 | def f(): -106 | # Invalid - nonexistent error code with multibyte character - - d = 1 # …noqa: F841, E50 -107 + d = 1 # noqa: F841 -108 | e = 1 # …noqa: E50 -109 | -110 | - F841 [*] Local variable `e` is assigned to but never used --> RUF100_0.py:108:5 | @@ -290,25 +271,7 @@ help: Remove assignment to unused variable `e` 110 | def f(): note: This is an unsafe fix and may change runtime behavior -RUF100 [*] Unused `noqa` directive (unknown: `E50`) - --> RUF100_0.py:108:12 - | -106 | # Invalid - nonexistent error code with multibyte character -107 | d = 1 # …noqa: F841, E50 -108 | e = 1 # …noqa: E50 - | ^^^^^^^^^^^ - | -help: Remove unused `noqa` directive -105 | def f(): -106 | # Invalid - nonexistent error code with multibyte character -107 | d = 1 # …noqa: F841, E50 - - e = 1 # …noqa: E50 -108 + e = 1 -109 | -110 | -111 | def f(): - -RUF100 [*] Unused `noqa` directive (duplicated: `F841`; unknown: `X200`) +RUF100 [*] Unused `noqa` directive (duplicated: `F841`) --> RUF100_0.py:118:12 | 116 | # Check duplicate code detection diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index ec09f363dcf01..aff7a8e0c5b42 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -274,7 +274,6 @@ impl Suppressions { .iter() .map(ToString::to_string) .collect_vec(), - ..Default::default() }), kind: UnusedNOQAKind::Suppression, }, From c2eb311ec3a8d89faf93c991979dd7c22c4f3c69 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 25 Feb 2026 15:58:29 -0800 Subject: [PATCH 094/261] Drop explicit support for `.qmd` file extension (#23572) --- crates/ruff_python_ast/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index f35b8c189fb2b..bd49f93b5edde 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -51,7 +51,7 @@ impl SourceType { pub fn from_extension(ext: &str) -> Self { match ext { "toml" => Self::Toml(TomlSourceType::Unrecognized), - "md" | "qmd" => Self::Markdown, + "md" => Self::Markdown, _ => Self::Python(PySourceType::from_extension(ext)), } } From 6ff4da454c448f01eb0b71728f3941870c811bfc Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 26 Feb 2026 00:23:30 +0000 Subject: [PATCH 095/261] [ty] Remove many `PartialOrd`/`Ord` implementations (#23573) --- .../src/diagnostic/levenshtein.rs | 2 +- .../src/semantic_index/scope.rs | 1 - crates/ty_python_semantic/src/types.rs | 81 ++----------------- crates/ty_python_semantic/src/types/class.rs | 42 +--------- .../ty_python_semantic/src/types/function.rs | 16 +--- .../ty_python_semantic/src/types/generics.rs | 10 --- .../ty_python_semantic/src/types/instance.rs | 18 +---- .../ty_python_semantic/src/types/literal.rs | 21 +---- .../ty_python_semantic/src/types/newtype.rs | 12 +-- .../src/types/protocol_class.rs | 14 +--- .../src/types/special_form.rs | 10 +-- crates/ty_python_semantic/src/types/tuple.rs | 4 - .../src/types/typed_dict.rs | 13 +-- 13 files changed, 25 insertions(+), 219 deletions(-) diff --git a/crates/ty_python_semantic/src/diagnostic/levenshtein.rs b/crates/ty_python_semantic/src/diagnostic/levenshtein.rs index 5de67d10dfeab..d1f3f0c54e98d 100644 --- a/crates/ty_python_semantic/src/diagnostic/levenshtein.rs +++ b/crates/ty_python_semantic/src/diagnostic/levenshtein.rs @@ -106,7 +106,7 @@ fn substitution_cost(char_a: char, char_b: char) -> CharacterMatch { } /// The result of comparing two characters. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum CharacterMatch { Exact, CaseInsensitive, diff --git a/crates/ty_python_semantic/src/semantic_index/scope.rs b/crates/ty_python_semantic/src/semantic_index/scope.rs index b8f0003043417..df10f6e7dd660 100644 --- a/crates/ty_python_semantic/src/semantic_index/scope.rs +++ b/crates/ty_python_semantic/src/semantic_index/scope.rs @@ -16,7 +16,6 @@ use crate::{ /// A cross-module identifier of a scope that can be used as a salsa query parameter. #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct ScopeId<'db> { pub file: File, diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 531472f261f41..daf2cb344a0ce 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -442,12 +442,7 @@ use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelati pub(crate) use todo_type; /// Represents an instance of `builtins.property`. -/// -/// # Ordering -/// Ordering is based on the property instance's salsa-assigned id and not on its values. -/// The id may change between runs, or when the property instance was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct PropertyInstanceType<'db> { getter: Option>, setter: Option>, @@ -532,7 +527,7 @@ bitflags! { /// For the precise meaning of the fields, see [1]. /// /// [1]: https://docs.python.org/3/library/dataclasses.html - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DataclassFlags: u16 { const INIT = 1 << 0; const REPR = 1 << 1; @@ -603,7 +598,6 @@ impl From for DataclassFlags { /// instance that we use as the return type of a `dataclasses.dataclass` and /// dataclass-transformer decorator calls. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct DataclassParams<'db> { flags: DataclassFlags, @@ -7215,7 +7209,6 @@ impl<'db> TypeMapping<'_, 'db> { /// sufficient. However, we currently think that tracked structs are unsound w.r.t. salsa cycles, /// so out of an abundance of caution, we are interning the struct. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct InternedConstraintSet<'db> { constraints: ConstraintSet<'db>, } @@ -7234,15 +7227,7 @@ impl get_size2::GetSize for InternedConstraintSet<'_> {} /// are generally created by operations at runtime in some way, such as a type alias /// statement, a typevar definition, or an instance of `Generic[T]` in a class's /// bases list. -/// -/// # Ordering -/// -/// Ordering between variants is stable and should be the same between runs. -/// Ordering within variants is based on the wrapped data's salsa-assigned id and not on its values. -/// The id may change between runs, or when e.g. a `TypeVarInstance` was garbage-collected and recreated. -#[derive( - Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, Ord, PartialOrd, get_size2::GetSize, -)] +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] pub enum KnownInstanceType<'db> { /// The type of `Protocol[T]`, `Protocol[U, S]`, etc -- usually only found in a class's bases list. /// @@ -7475,7 +7460,7 @@ impl<'db> KnownInstanceType<'db> { /// (e.g. `Divergent` is assignable to `@Todo`, but `@Todo | Divergent` must not be reducted to `@Todo`). /// Otherwise, type inference cannot converge properly. /// For detailed properties of this type, see the unit test at the end of the file. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, salsa::Update)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct DivergentType { /// The query ID that caused the cycle. id: salsa::Id, @@ -7899,7 +7884,6 @@ impl<'db> InvalidTypeExpression<'db> { /// Data regarding a `warnings.deprecated` or `typing_extensions.deprecated` decorator. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct DeprecatedInstance<'db> { /// The message for the deprecation pub message: Option>, @@ -7911,7 +7895,6 @@ impl get_size2::GetSize for DeprecatedInstance<'_> {} /// Contains information about instances of `dataclasses.Field`, typically created using /// `dataclasses.field()`. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct FieldInstance<'db> { /// The type of the default value for this field. This is derived from the `default` or /// `default_factory` arguments to `dataclasses.field()`. @@ -8053,12 +8036,7 @@ impl<'db> TypeVarIdentity<'db> { /// the typevar is defined and immediately bound to a single generic context. Just like in the /// legacy case, we will create a `TypeVarInstance` and [`BoundTypeVarInstance`], and the type of /// `T` at `[1]` and `[2]` will be that `TypeVarInstance` and `BoundTypeVarInstance`, respectively. -/// -/// # Ordering -/// Ordering is based on the type var instance's salsa-assigned id and not on its values. -/// The id may change between runs, or when the type var instance was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct TypeVarInstance<'db> { /// The identity of this typevar pub(crate) identity: TypeVarIdentity<'db>, @@ -8599,13 +8577,7 @@ pub struct BoundTypeVarIdentity<'db> { /// A type variable that has been bound to a generic context, and which can be specialized to a /// concrete type. -/// -/// # Ordering -/// -/// Ordering is based on the wrapped data's salsa-assigned id and not on its values. -/// The id may change between runs, or when e.g. a `BoundTypeVarInstance` was garbage-collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct BoundTypeVarInstance<'db> { pub typevar: TypeVarInstance<'db>, binding_context: BindingContext<'db>, @@ -9159,12 +9131,7 @@ impl InferredAs { /// Contains information about a `types.UnionType` instance built from a PEP 604 /// union or a legacy `typing.Union[…]` annotation in a value expression context, /// e.g. `IntOrStr = int | str` or `IntOrStr = Union[int, str]`. -/// -/// # Ordering -/// Ordering is based on the context's salsa-assigned id and not on its values. -/// The id may change between runs, or when the context was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct UnionTypeInstance<'db> { /// The types of the elements of this union, as they were inferred in a value /// expression context. For `int | str`, this would contain `` and @@ -9281,12 +9248,7 @@ impl<'db> UnionTypeInstance<'db> { } /// A salsa-interned `Type` -/// -/// # Ordering -/// Ordering is based on the context's salsa-assigned id and not on its values. -/// The id may change between runs, or when the context was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct InternedType<'db> { inner: Type<'db>, } @@ -10257,12 +10219,7 @@ impl From for Truthiness { /// on an instance of a class. For example, the expression `Path("a.txt").touch` creates /// a bound method object that represents the `Path.touch` method which is bound to the /// instance `Path("a.txt")`. -/// -/// # Ordering -/// Ordering is based on the bounded method's salsa-assigned id and not on its values. -/// The id may change between runs, or when the bounded method was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct BoundMethodType<'db> { /// The function that is being bound. Corresponds to the `__func__` attribute on a /// bound method object @@ -10381,7 +10338,7 @@ impl<'db> BoundMethodType<'db> { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum CallableTypeKind { /// Represents regular callable objects. Regular, @@ -10409,12 +10366,7 @@ pub enum CallableTypeKind { /// It can be written in type expressions using `typing.Callable`. `lambda` expressions are /// inferred directly as `CallableType`s; all function-literal types are subtypes of a /// `CallableType`. -/// -/// # Ordering -/// Ordering is based on the callable type's salsa-assigned id and not on its values. -/// The id may change between runs, or when the callable type was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct CallableType<'db> { #[returns(ref)] pub(crate) signatures: CallableSignature<'db>, @@ -10676,9 +10628,7 @@ impl<'db> CallableTypes<'db> { /// /// Unlike bound methods of user-defined classes, these are not generally instances /// of `types.BoundMethodType` at runtime. -#[derive( - Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update, get_size2::GetSize, -)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub enum KnownBoundMethodType<'db> { /// Method wrapper for `some_function.__get__` FunctionTypeDunderGet(FunctionType<'db>), @@ -11111,9 +11061,7 @@ impl<'db> KnownBoundMethodType<'db> { } /// Represents a specific instance of `types.WrapperDescriptorType` -#[derive( - Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update, get_size2::GetSize, -)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub enum WrapperDescriptorKind { /// `FunctionType.__get__` FunctionTypeDunderGet, @@ -11202,11 +11150,7 @@ impl WrapperDescriptorKind { } } -/// # Ordering -/// Ordering is based on the module literal's salsa-assigned id and not on its values. -/// The id may change between runs, or when the module literal was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct ModuleLiteralType<'db> { /// The imported module. pub module: Module<'db>, @@ -11365,11 +11309,7 @@ impl<'db> ModuleLiteralType<'db> { } } -/// # Ordering -/// Ordering is based on the type alias's salsa-assigned id and not on its values. -/// The id may change between runs, or when the alias was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct PEP695TypeAliasType<'db> { #[returns(ref)] pub name: ast::name::Name, @@ -11484,12 +11424,7 @@ impl<'db> PEP695TypeAliasType<'db> { /// /// The value type is computed lazily via [`ManualPEP695TypeAliasType::value_type()`] /// to avoid cycle non-convergence for mutually recursive definitions. -/// -/// # Ordering -/// Ordering is based on the type alias's salsa-assigned id and not on its values. -/// The id may change between runs, or when the alias was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct ManualPEP695TypeAliasType<'db> { #[returns(ref)] pub name: ast::name::Name, @@ -11539,9 +11474,7 @@ impl<'db> ManualPEP695TypeAliasType<'db> { } } -#[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update, get_size2::GetSize, -)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub enum TypeAliasType<'db> { /// A type alias defined using the PEP 695 `type` statement. PEP695(PEP695TypeAliasType<'db>), diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 90ef014b08310..ca6a95a1dec99 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -267,12 +267,7 @@ impl<'db> CodeGeneratorKind<'db> { } /// A specialization of a generic class with a particular assignment of types to typevars. -/// -/// # Ordering -/// Ordering is based on the generic aliases's salsa-assigned id and not on its values. -/// The id may change between runs, or when the alias was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct GenericAlias<'db> { pub(crate) origin: StaticClassLiteral<'db>, pub(crate) specialization: Specialization<'db>, @@ -400,17 +395,7 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> { /// A class literal, either defined via a `class` statement or a `type` function call. #[derive( - Clone, - Copy, - Debug, - Eq, - Hash, - Ord, - PartialEq, - PartialOrd, - salsa::Supertype, - salsa::Update, - get_size2::GetSize, + Clone, Copy, Debug, Eq, Hash, PartialEq, salsa::Supertype, salsa::Update, get_size2::GetSize, )] pub enum ClassLiteral<'db> { /// A class defined via a `class` statement. @@ -792,17 +777,7 @@ impl<'db> From> for ClassLiteral<'db> { /// Represents a class type, which might be a non-generic class, or a specialization of a generic /// class. #[derive( - Clone, - Copy, - Debug, - Eq, - Hash, - Ord, - PartialEq, - PartialOrd, - salsa::Supertype, - salsa::Update, - get_size2::GetSize, + Clone, Copy, Debug, Eq, Hash, PartialEq, salsa::Supertype, salsa::Update, get_size2::GetSize, )] pub enum ClassType<'db> { // `NonGeneric` is intended to mean that the `ClassLiteral` has no type parameters. There are @@ -2089,12 +2064,7 @@ impl<'db> Field<'db> { /// /// This does not in itself represent a type, but can be transformed into a [`ClassType`] that /// does. (For generic classes, this requires specializing its generic context.) -/// -/// # Ordering -/// Ordering is based on the class's id assigned by salsa and not on the class literal's values. -/// The id may change between runs, or when the class literal was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct StaticClassLiteral<'db> { /// Name of the class at definition #[returns(ref)] @@ -5039,7 +5009,6 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope /// provides stable identity that only changes when the scope itself changes. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct DynamicClassLiteral<'db> { /// The name of the class (from the first argument to `type()`). #[returns(ref)] @@ -5612,7 +5581,6 @@ pub struct NamedTupleField<'db> { /// /// The type of `Point` would be `type[Point]` where `Point` is a `DynamicNamedTupleLiteral`. #[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct DynamicNamedTupleLiteral<'db> { /// The name of the namedtuple (from the first argument). #[returns(ref)] @@ -6003,13 +5971,7 @@ pub enum DynamicNamedTupleAnchor<'db> { /// A specification describing the fields of a dynamic `namedtuple` /// or `NamedTuple` class. -/// -/// # Ordering -/// -/// Ordering is based on the spec's salsa-assigned id and not on its values. -/// The id may change between runs, or when the spec was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct NamedTupleSpec<'db> { #[returns(deref)] pub(crate) fields: Box<[NamedTupleField<'db>]>, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 53d30e551d694..f735af5e273f7 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -160,7 +160,7 @@ bitflags! { /// arguments that were passed in. For the precise meaning of the fields, see [1]. /// /// [1]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, salsa::Update)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct DataclassTransformerFlags: u8 { const EQ_DEFAULT = 1 << 0; const ORDER_DEFAULT = 1 << 1; @@ -180,7 +180,6 @@ impl Default for DataclassTransformerFlags { /// Metadata for a dataclass-transformer. Stored inside a `Type::DataclassTransformer(…)` /// instance that we use as the return type for `dataclass_transform(…)` calls. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct DataclassTransformerParams<'db> { pub flags: DataclassTransformerFlags, @@ -205,13 +204,7 @@ pub(crate) fn is_implicit_classmethod(function_name: &str) -> bool { /// /// If a function has multiple overloads, each overload is represented by a separate function /// definition in the AST, and is therefore a separate `OverloadLiteral` instance. -/// -/// # Ordering -/// Ordering is based on the function's id assigned by salsa and not on the function literal's -/// values. The id may change between runs, or when the function literal was garbage collected and -/// recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct OverloadLiteral<'db> { /// Name of the function at definition. #[returns(ref)] @@ -649,13 +642,7 @@ impl<'db> OverloadLiteral<'db> { /// Representation of a function definition in the AST, along with any previous overloads of the /// function. Each overload can be separately generic or not, and each generic overload uses /// distinct typevars. -/// -/// # Ordering -/// Ordering is based on the function's id assigned by salsa and not on the function literal's -/// values. The id may change between runs, or when the function literal was garbage collected and -/// recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct FunctionLiteral<'db> { pub(crate) last_definition: OverloadLiteral<'db>, } @@ -889,7 +876,6 @@ impl AbstractMethodKind { /// Represents a function type, which might be a non-generic function, or a specialization of a /// generic function. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct FunctionType<'db> { pub(crate) literal: FunctionLiteral<'db>, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index d6a7eef824650..75df307bf439f 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -267,12 +267,7 @@ impl<'a, 'db> InferableTypeVars<'a, 'db> { } /// A list of formal type variables for a generic function, class, or type alias. -/// -/// # Ordering -/// Ordering is based on the context's salsa-assigned id and not on its values. -/// The id may change between runs, or when the context was garbage collected and recreated. #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct GenericContext<'db> { #[returns(ref)] variables_inner: FxOrderMap, BoundTypeVarInstance<'db>>, @@ -957,12 +952,7 @@ impl<'db> GenericContext<'db> { /// /// TODO: Handle nested specializations better, with actual parent links to the specialization of /// the lexically containing context. -/// -/// # Ordering -/// Ordering is based on the context's salsa-assigned id and not on its values. -/// The id may change between runs, or when the context was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct Specialization<'db> { pub(crate) generic_context: GenericContext<'db>, #[returns(deref)] diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index bd2a5c9757a59..4873ee1fb07e5 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -613,9 +613,7 @@ impl<'db> VarianceInferable<'db> for NominalInstanceType<'db> { /// A `ProtocolInstanceType` represents the set of all possible runtime objects /// that conform to the interface described by a certain protocol. -#[derive( - Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, -)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] pub struct ProtocolInstanceType<'db> { pub(super) inner: Protocol<'db>, @@ -810,15 +808,7 @@ impl<'db> VarianceInferable<'db> for ProtocolInstanceType<'db> { /// An enumeration of the two kinds of protocol types: those that originate from a class /// definition in source code, and those that are synthesized from a set of members. -/// -/// # Ordering -/// -/// Ordering between variants is stable and should be the same between runs. -/// Ordering within variants is based on the wrapped data's salsa-assigned id and not on its values. -/// The id may change between runs, or when e.g. a `Protocol` was garbage-collected and recreated. -#[derive( - Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, -)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] pub(super) enum Protocol<'db> { FromClass(ProtocolClass<'db>), Synthesized(SynthesizedProtocolType<'db>), @@ -871,9 +861,7 @@ mod synthesized_protocol { use crate::{Db, FxOrderSet}; /// A "synthesized" protocol type that is dissociated from a class definition in source code. - #[derive( - Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, - )] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] pub(in crate::types) struct SynthesizedProtocolType<'db>(ProtocolInterface<'db>); impl<'db> SynthesizedProtocolType<'db> { diff --git a/crates/ty_python_semantic/src/types/literal.rs b/crates/ty_python_semantic/src/types/literal.rs index 57aa957f873aa..68ce81262c379 100644 --- a/crates/ty_python_semantic/src/types/literal.rs +++ b/crates/ty_python_semantic/src/types/literal.rs @@ -5,16 +5,12 @@ use crate::Db; use crate::types::{ClassLiteral, KnownClass, Type}; /// A literal value. See [`LiteralValueTypeKind`] for details. -#[derive( - PartialOrd, Ord, Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize, -)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub struct LiteralValueType<'db>(LiteralValueTypeInner<'db>); // This enum effectively contains two variants, `Promotable(LiteralValueKind)` and `Unpromotable(LiteralValueKind)`, // but flattened to reduce the size of the type. -#[derive( - PartialOrd, Ord, Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize, -)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] enum LiteralValueTypeInner<'db> { PromotableInt(IntLiteralType), PromotableBool(bool), @@ -30,9 +26,7 @@ enum LiteralValueTypeInner<'db> { UnpromotableLiteralString, } -#[derive( - PartialOrd, Ord, Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize, -)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(crate) enum LiteralValueTypeKind<'db> { /// An integer literal Int(IntLiteralType), @@ -311,11 +305,7 @@ impl std::fmt::Debug for IntLiteralType { } } -/// # Ordering -/// Ordering is based on the string literal's salsa-assigned id and not on its value. -/// The id may change between runs, or when the string literal was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct StringLiteralType<'db> { #[returns(deref)] pub(crate) value: CompactString, @@ -331,11 +321,7 @@ impl<'db> StringLiteralType<'db> { } } -/// # Ordering -/// Ordering is based on the byte literal's salsa-assigned id and not on its value. -/// The id may change between runs, or when the byte literal was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct BytesLiteralType<'db> { #[returns(deref)] pub(crate) value: Box<[u8]>, @@ -360,7 +346,6 @@ impl<'db> BytesLiteralType<'db> { /// YES = 1 /// ``` #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct EnumLiteralType<'db> { /// A reference to the enum class this literal belongs to pub(crate) enum_class: ClassLiteral<'db>, diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index 906999a9f2d60..afd5fcfb314c0 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -1,11 +1,10 @@ -use std::collections::BTreeSet; - use crate::Db; use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::types::constraints::ConstraintSet; use crate::types::{ClassType, KnownUnion, Type, definition_expression_type, visitor}; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; +use rustc_hash::FxHashSet; /// A `typing.NewType` declaration, either from the perspective of the /// identity-callable-that-acts-like-a-subtype-in-type-expressions returned by the call to @@ -22,12 +21,7 @@ use ruff_python_ast as ast; /// - `typing.NewType`: `Type::ClassLiteral(ClassLiteral)` with `KnownClass::NewType`. /// - `Foo`: `Type::KnownInstance(KnownInstanceType::NewType(NewType { .. }))` /// - `x`: `Type::NewTypeInstance(NewType { .. })` -/// -/// # Ordering -/// Ordering is based on the newtype's salsa-assigned id and not on its values. -/// The id may change between runs, or when the newtype was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct NewType<'db> { /// The name of this NewType (e.g. `"Foo"`) #[returns(ref)] @@ -96,7 +90,7 @@ impl<'db> NewType<'db> { fn iter_bases(self, db: &'db dyn Db) -> NewTypeBaseIter<'db> { NewTypeBaseIter { current: Some(self), - seen_before: BTreeSet::new(), + seen_before: FxHashSet::default(), db, } } @@ -256,7 +250,7 @@ impl<'db> NewTypeBase<'db> { /// over the base class need to pass down a cycle-detecting visitor as usual. struct NewTypeBaseIter<'db> { current: Option>, - seen_before: BTreeSet>, + seen_before: FxHashSet>, db: &'db dyn Db, } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 1f18b68812fc9..dd8832898de7f 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -45,14 +45,7 @@ impl<'db> ClassType<'db> { } /// Representation of a single `Protocol` class definition. -/// -/// # Ordering -/// -/// Ordering is based on the wrapped data's salsa-assigned id and not on its values. -/// The id may change between runs, or when e.g. a `ProtocolClass` was garbage-collected and recreated. -#[derive( - Debug, Copy, Clone, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize, PartialOrd, Ord, -)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(super) struct ProtocolClass<'db>(ClassType<'db>); impl<'db> ProtocolClass<'db> { @@ -184,12 +177,7 @@ impl<'db> From> for Type<'db> { } /// The interface of a protocol: the members of that protocol, and the types of those members. -/// -/// # Ordering -/// Ordering is based on the protocol interface member's salsa-assigned id and not on its members. -/// The id may change between runs, or when the protocol instance members was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub(super) struct ProtocolInterface<'db> { #[returns(ref)] inner: BTreeMap>, diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 5e6e3c9a5a8da..96fc2f2dceff6 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -22,11 +22,7 @@ use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; /// The enum uses a nested structure: variants that fall into well-defined subcategories /// (legacy stdlib aliases and type qualifiers) are represented as nested enums, /// while other special forms that each require unique handling remain as direct variants. -/// -/// # Ordering -/// -/// Ordering is stable and should be the same between runs. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum SpecialFormType { /// Special forms that are simple aliases to classes elsewhere in the standard library. LegacyStdlibAlias(LegacyStdlibAlias), @@ -762,7 +758,7 @@ impl std::fmt::Display for SpecialFormType { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum LegacyStdlibAlias { List, Dict, @@ -812,7 +808,7 @@ impl std::fmt::Display for LegacyStdlibAlias { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum TypeQualifier { ReadOnly, Final, diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 457e3179b2841..8c3155f5c0e97 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -126,11 +126,7 @@ impl TupleLength { } } -/// # Ordering -/// Ordering is based on the tuple's salsa-assigned id and not on its elements. -/// The id may change between runs, or when the tuple was garbage collected and recreated. #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct TupleType<'db> { #[returns(ref)] pub(crate) tuple: TupleSpec<'db>, diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index f0898a856c6ba..c75c608cda593 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -47,14 +47,7 @@ impl Default for TypedDictParams { /// Type that represents the set of all inhabitants (`dict` instances) that conform to /// a given `TypedDict` schema. -/// -/// # Ordering -/// Ordering is derived from the variant order (`Class` < `Synthesized`) and the inner types. -/// The Salsa IDs of inner types may change between runs or when the type was garbage collected -/// and recreated. -#[derive( - Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, salsa::Update, Hash, get_size2::GetSize, -)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash, get_size2::GetSize)] pub enum TypedDictType<'db> { /// A reference to the class (inheriting from `typing.TypedDict`) that specifies the /// schema of this `TypedDict`. @@ -1021,11 +1014,7 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( } } -/// # Ordering -/// Ordering is based on the type's salsa-assigned id and not on its values. -/// The id may change between runs, or when the type was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct SynthesizedTypedDictType<'db> { #[returns(ref)] pub(crate) items: TypedDictSchema<'db>, From 14bd2b27c28aee254b6dbcae3583f978f9eacd4f Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:26:54 +0100 Subject: [PATCH 096/261] [ty] support enum `_value_` annotation (#22228) ## Summary The` _value_` attribute is used inside an Enum class to explicitly define the underlying value of an enum member. Typing can be verified on the members. ## Test Plan Added a mdtest --------- Co-authored-by: Carl Meyer Co-authored-by: Charlie Marsh --- .../resources/mdtest/enums.md | 247 +++++++++++++++++- crates/ty_python_semantic/src/types.rs | 8 +- crates/ty_python_semantic/src/types/enums.rs | 122 ++++++++- .../ty_python_semantic/src/types/overrides.rs | 147 ++++++++++- 4 files changed, 497 insertions(+), 27 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index c3b1e55c53e85..90f59bad8e520 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -100,6 +100,195 @@ class Answer(Enum): reveal_type(enum_members(Answer)) ``` +### Declared `_value_` annotation + +If a `_value_` annotation is defined on an `Enum` class, all enum member values must be compatible +with the declared type: + +```pyi +from enum import Enum + +class Color(Enum): + _value_: int + RED = 1 + GREEN = "green" # error: [invalid-assignment] + BLUE = ... + YELLOW = None # error: [invalid-assignment] + PURPLE = [] # error: [invalid-assignment] +``` + +When `_value_` is annotated, `.value` and `._value_` are inferred as the declared type: + +```py +from enum import Enum +from typing import Final + +class Color2(Enum): + _value_: int + RED = 1 + GREEN = 2 + +reveal_type(Color2.RED.value) # revealed: int +reveal_type(Color2.RED._value_) # revealed: int + +class WantsInt(Enum): + _value_: int + OK: Final = 1 + BAD: Final = "oops" # error: [invalid-assignment] +``` + +### `_value_` annotation with `__init__` + +When `__init__` is defined, member values are validated by synthesizing a call to `__init__`. The +`_value_` annotation still constrains assignments to `self._value_` inside `__init__`: + +```py +from enum import Enum + +class Planet(Enum): + _value_: int + + def __init__(self, value: int, mass: float, radius: float): + self._value_ = value + + MERCURY = (1, 3.303e23, 2.4397e6) + SATURN = "saturn" # error: [invalid-assignment] + +reveal_type(Planet.MERCURY.value) # revealed: int +reveal_type(Planet.MERCURY._value_) # revealed: int +``` + +`Final`-annotated members are also validated against `__init__`: + +```py +from enum import Enum +from typing import Final + +class Planet(Enum): + def __init__(self, mass: float, radius: float): + self.mass = mass + self.radius = radius + + MERCURY: Final = (3.303e23, 2.4397e6) + BAD: Final = "not a planet" # error: [invalid-assignment] +``` + +### `_value_` annotation incompatible with `__init__` + +When `_value_` and `__init__` disagree, the assignment inside `__init__` is flagged: + +```py +from enum import Enum + +class Planet(Enum): + _value_: str + + def __init__(self, value: int, mass: float, radius: float): + self._value_ = value # error: [invalid-assignment] + + MERCURY = (1, 3.303e23, 2.4397e6) + SATURN = "saturn" # error: [invalid-assignment] + +reveal_type(Planet.MERCURY.value) # revealed: str +reveal_type(Planet.MERCURY._value_) # revealed: str +``` + +### `__init__` without `_value_` annotation + +When `__init__` is defined but no explicit `_value_` annotation exists, member values are validated +against the `__init__` signature. Values that are incompatible with `__init__` are flagged: + +```py +from enum import Enum + +class Planet2(Enum): + def __init__(self, mass: float, radius: float): + self.mass = mass + self.radius = radius + + MERCURY = (3.303e23, 2.4397e6) + VENUS = (4.869e24, 6.0518e6) + INVALID = "not a planet" # error: [invalid-assignment] + +reveal_type(Planet2.MERCURY.value) # revealed: Any +reveal_type(Planet2.MERCURY._value_) # revealed: Any +``` + +### Inherited `_value_` annotation + +A `_value_` annotation on a parent enum is inherited by subclasses. Member values are validated +against the inherited annotation, and `.value` uses the declared type: + +```py +from enum import Enum + +class Base(Enum): + _value_: int + +class Child(Base): + A = 1 + B = "not an int" # error: [invalid-assignment] + +reveal_type(Child.A.value) # revealed: int +``` + +This also works through multiple levels of inheritance, where `_value_` is declared on an +intermediate class: + +```py +from enum import Enum + +class Grandparent(Enum): + pass + +class Parent(Grandparent): + _value_: int + +class Child(Parent): + A = 1 + B = "not an int" # error: [invalid-assignment] + +reveal_type(Child.A.value) # revealed: int +``` + +### Inherited `__init__` + +A custom `__init__` on a parent enum is inherited by subclasses. Member values are validated against +the inherited `__init__` signature: + +```py +from enum import Enum + +class Base(Enum): + def __init__(self, a: int, b: str): + self._value_ = a + +class Child(Base): + A = (1, "foo") + B = "should be checked against __init__" # error: [invalid-assignment] + +reveal_type(Child.A.value) # revealed: Any +``` + +This also works through multiple levels of inheritance: + +```py +from enum import Enum + +class Grandparent(Enum): + def __init__(self, a: int, b: str): + self._value_ = a + +class Parent(Grandparent): + pass + +class Child(Parent): + A = (1, "foo") + B = "bad" # error: [invalid-assignment] + +reveal_type(Child.A.value) # revealed: Any +``` + ### Non-member attributes with disallowed type Methods, callables, descriptors (including properties), and nested classes that are defined in the @@ -358,7 +547,8 @@ class SingleMember(StrEnum): reveal_type(SingleMember.SINGLE.value) # revealed: Literal["single"] ``` -Using `auto()` with `IntEnum` also works as expected: +Using `auto()` with `IntEnum` also works as expected. `IntEnum` declares `_value_: int` in typeshed, +so `.value` is typed as `int` rather than a precise literal: ```py from enum import IntEnum, auto @@ -367,8 +557,8 @@ class Answer(IntEnum): YES = auto() NO = auto() -reveal_type(Answer.YES.value) # revealed: Literal[1] -reveal_type(Answer.NO.value) # revealed: Literal[2] +reveal_type(Answer.YES.value) # revealed: int +reveal_type(Answer.NO.value) # revealed: int ``` As does using `auto()` for other enums that use `int` as a mixin: @@ -433,6 +623,30 @@ class Answer(Enum): reveal_type(enum_members(Answer)) ``` +`auto()` values are computed at runtime by the enum metaclass, so we skip validation against both +`_value_` annotations and custom `__init__` signatures: + +```py +from enum import Enum, auto + +class WithValue(Enum): + _value_: int + A = auto() + B = auto() + +reveal_type(WithValue.A.value) # revealed: int + +class WithInit(Enum): + def __init__(self, mass: float, radius: float): + self.mass = mass + self.radius = radius + + MERCURY = (3.303e23, 2.4397e6) + AUTO = auto() + +reveal_type(WithInit.MERCURY.value) # revealed: Any +``` + ### `member` and `nonmember` ```toml @@ -475,13 +689,14 @@ class Answer(Enum): reveal_type(enum_members(Answer)) ``` -### Class-private names +### Dunder and class-private names -An attribute with a [class-private name] (beginning with, but not ending in, a double underscore) is -treated as a non-member: +An attribute with a name beginning with a double underscore is treated as a non-member. This +includes both [class-private names] (not ending in `__`) and dunder names (ending in `__`). +CPython's enum metaclass excludes all such names from membership: ```py -from enum import Enum +from enum import Enum, IntEnum from ty_extensions import enum_members class Answer(Enum): @@ -491,10 +706,24 @@ class Answer(Enum): __private_member = 3 __maybe__ = 4 -# revealed: tuple[Literal["YES"], Literal["NO"], Literal["__maybe__"]] +# revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` +Setting `__module__` (a common pattern to control `repr()` and `pickle` behavior) does not make it +an enum member, even when the value type differs from the enum's value type: + +```py +class ExitCode(IntEnum): + OK = 0 + ERROR = 1 + + __module__ = "my_package" # no error, not a member + +# revealed: tuple[Literal["OK"], Literal["ERROR"]] +reveal_type(enum_members(ExitCode)) +``` + ### Ignored names An enum class can define a class symbol named `_ignore_`. This can be a string containing a @@ -1123,4 +1352,4 @@ class MyEnum[T](MyEnumBase): - Typing spec: - Documentation: -[class-private name]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers +[class-private names]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index daf2cb344a0ce..2e6bb6dfccacc 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -3335,7 +3335,7 @@ impl<'db> Type<'db> { { let enum_literal = literal.as_enum().unwrap(); enum_metadata(db, enum_literal.enum_class(db)) - .and_then(|metadata| metadata.members.get(enum_literal.name(db))) + .and_then(|metadata| metadata.value_type(enum_literal.name(db))) .map_or_else(|| Place::Undefined, Place::bound) .into() } @@ -3359,10 +3359,10 @@ impl<'db> Type<'db> { { enum_metadata(db, instance.class_literal(db)) .and_then(|metadata| { - let (_, ty) = metadata.members.get_index(0)?; - Some(Place::bound(*ty)) + let (name, _) = metadata.members.get_index(0)?; + metadata.value_type(name) }) - .unwrap_or_default() + .map_or_else(Place::default, Place::bound) .into() } diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 50b80465043b4..19d920662914b 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -1,5 +1,5 @@ use ruff_python_ast::name::Name; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use crate::{ @@ -7,10 +7,10 @@ use crate::{ place::{ DefinedPlace, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations, }, - semantic_index::{place_table, use_def_map}, + semantic_index::{place_table, scope::ScopeId, use_def_map}, types::{ ClassBase, ClassLiteral, DynamicType, EnumLiteralType, KnownClass, LiteralValueTypeKind, - MemberLookupPolicy, StaticClassLiteral, Type, TypeQualifiers, + MemberLookupPolicy, StaticClassLiteral, Type, TypeQualifiers, function::FunctionType, }, }; @@ -18,15 +18,47 @@ use crate::{ pub(crate) struct EnumMetadata<'db> { pub(crate) members: FxIndexMap>, pub(crate) aliases: FxHashMap, + + /// Members whose values were defined using `auto()`. + pub(crate) auto_members: FxHashSet, + + /// The explicit `_value_` annotation type, if declared. + pub(crate) value_annotation: Option>, + + /// The custom `__init__` function, if defined on this enum. + /// + /// When present, member values are validated by synthesizing a call to + /// `__init__` rather than by simple type assignability. + pub(crate) init_function: Option>, } impl get_size2::GetSize for EnumMetadata<'_> {} -impl EnumMetadata<'_> { +impl<'db> EnumMetadata<'db> { fn empty() -> Self { EnumMetadata { members: FxIndexMap::default(), aliases: FxHashMap::default(), + auto_members: FxHashSet::default(), + value_annotation: None, + init_function: None, + } + } + + /// Returns the type of `.value`/`._value_` for a given enum member. + /// + /// Priority: explicit `_value_` annotation, then `__init__` → `Any`, + /// then the inferred member value type. + pub(crate) fn value_type(&self, member_name: &Name) -> Option> { + if !self.members.contains_key(member_name) { + return None; + } + if let Some(annotation) = self.value_annotation { + Some(annotation) + } else if self.init_function.is_some() { + Some(Type::Dynamic(DynamicType::Any)) + } else { + self.members.get(member_name).copied() } } @@ -81,7 +113,7 @@ pub(crate) fn enum_metadata<'db>( let mut enum_values: FxHashMap, Name> = FxHashMap::default(); let mut auto_counter = 0; - + let mut auto_members = FxHashSet::default(); let ignored_names: Option> = if let Some(ignore) = table.symbol_id("_ignore_") { let ignore_bindings = use_def_map.reachable_symbol_bindings(ignore); let ignore_place = place_from_bindings(db, ignore_bindings).place; @@ -105,8 +137,9 @@ pub(crate) fn enum_metadata<'db>( .filter_map(|(symbol_id, bindings)| { let name = table.symbol(symbol_id).name(); - if name.starts_with("__") && !name.ends_with("__") { - // Skip private attributes + if name.starts_with("__") { + // Skip private attributes (`__private`) and dunders (`__module__`, etc.). + // CPython's enum metaclass never treats these as members. return None; } @@ -146,6 +179,7 @@ pub(crate) fn enum_metadata<'db>( // enum.auto Some(KnownClass::Auto) => { auto_counter += 1; + auto_members.insert(name.clone()); // `StrEnum`s have different `auto()` behaviour to enums inheriting from `(str, Enum)` let auto_value_ty = @@ -287,7 +321,79 @@ pub(crate) fn enum_metadata<'db>( return None; } - Some(EnumMetadata { members, aliases }) + // Look up an explicit `_value_` annotation, if present. Falls back to + // checking parent enum classes in the MRO. + let value_annotation = + custom_value_annotation(db, scope_id).or_else(|| inherited_value_annotation(db, class)); + + // Look up a custom `__init__`, falling back to parent enum classes. + let init_function = custom_init(db, scope_id).or_else(|| inherited_init(db, class)); + + Some(EnumMetadata { + members, + aliases, + auto_members, + value_annotation, + init_function, + }) +} + +/// Iterates over parent enum classes in the MRO, skipping known classes +/// (like `Enum`, `StrEnum`, etc.) that we handle specially. +fn iter_parent_enum_classes<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, +) -> impl Iterator> + 'db { + class + .iter_mro(db, None) + .skip(1) + .filter_map(ClassBase::into_class) + .filter_map(move |class_type| { + let base = class_type.class_literal(db).as_static()?; + (base.known(db).is_none() && is_enum_class_by_inheritance(db, base)).then_some(base) + }) +} + +/// Returns the `_value_` annotation type if one is declared in the given scope. +fn custom_value_annotation<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Option> { + let symbol_id = place_table(db, scope).symbol_id("_value_")?; + let declarations = use_def_map(db, scope).end_of_scope_symbol_declarations(symbol_id); + place_from_declarations(db, declarations) + .ignore_conflicting_declarations() + .ignore_possibly_undefined() +} + +/// Looks up an inherited `_value_` annotation from parent enum classes in the MRO. +fn inherited_value_annotation<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, +) -> Option> { + iter_parent_enum_classes(db, class) + .find_map(|base| custom_value_annotation(db, base.body_scope(db))) +} + +/// Looks up an inherited `__init__` from parent enum classes in the MRO. +fn inherited_init<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, +) -> Option> { + iter_parent_enum_classes(db, class).find_map(|base| custom_init(db, base.body_scope(db))) +} + +/// Returns the custom `__init__` function type if one is defined on the enum. +fn custom_init<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Option> { + let init_symbol_id = place_table(db, scope).symbol_id("__init__")?; + let init_type = place_from_declarations( + db, + use_def_map(db, scope).end_of_scope_symbol_declarations(init_symbol_id), + ) + .ignore_conflicting_declarations() + .ignore_possibly_undefined()?; + + match init_type { + Type::FunctionLiteral(f) => Some(f), + _ => None, + } } pub(crate) fn enum_member_literals<'a, 'db: 'a>( diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index eea86fe686354..5fbac45e8be75 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -23,17 +23,20 @@ use crate::{ }, types::{ CallableType, ClassBase, ClassType, KnownClass, Parameter, Parameters, Signature, - StaticClassLiteral, Type, TypeQualifiers, + StaticClassLiteral, Type, TypeContext, TypeQualifiers, + call::CallArguments, class::{CodeGeneratorKind, FieldKind}, context::InferContext, diagnostic::{ - INVALID_DATACLASS, INVALID_EXPLICIT_OVERRIDE, INVALID_METHOD_OVERRIDE, - INVALID_NAMED_TUPLE, OVERRIDE_OF_FINAL_METHOD, OVERRIDE_OF_FINAL_VARIABLE, - report_invalid_method_override, report_overridden_final_method, - report_overridden_final_variable, + INVALID_ASSIGNMENT, INVALID_DATACLASS, INVALID_EXPLICIT_OVERRIDE, + INVALID_METHOD_OVERRIDE, INVALID_NAMED_TUPLE, OVERRIDE_OF_FINAL_METHOD, + OVERRIDE_OF_FINAL_VARIABLE, report_invalid_method_override, + report_overridden_final_method, report_overridden_final_variable, }, + enums::{EnumMetadata, enum_metadata}, function::{FunctionDecorators, FunctionType, KnownFunction}, list_members::{Member, MemberWithDefinition, all_end_of_scope_members}, + tuple::Tuple, }, }; @@ -66,15 +69,24 @@ pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: StaticCla let class_specialized = class.identity_specialization(db); let scope = class.body_scope(db); let own_class_members: FxHashSet<_> = all_end_of_scope_members(db, scope).collect(); + let enum_info = enum_metadata(db, class.into()); for member in own_class_members { - check_class_declaration(context, configuration, class_specialized, scope, &member); + check_class_declaration( + context, + configuration, + enum_info, + class_specialized, + scope, + &member, + ); } } fn check_class_declaration<'db>( context: &InferContext<'db, '_>, configuration: OverrideRulesConfig, + enum_info: Option<&EnumMetadata<'db>>, class: ClassType<'db>, class_scope: ScopeId<'db>, member: &MemberWithDefinition<'db>, @@ -173,6 +185,67 @@ fn check_class_declaration<'db>( Some(CodeGeneratorKind::TypedDict) | None => {} } + // Check for invalid Enum member values. + if let Some(enum_info) = enum_info { + if member.name != "_value_" + && matches!( + first_reachable_definition.kind(db), + DefinitionKind::Assignment(_) | DefinitionKind::AnnotatedAssignment(_) + ) + { + // Use the value type from `EnumMetadata` rather than `member.ty`, because + // for annotated assignments like `X: Final = "value"`, the member may come + // from the declaration chain (where `ty` is the declared type, e.g. `Unknown`) + // rather than the binding chain (where `ty` is the actual value type). + let Some(&member_value_type) = enum_info.members.get(&member.name) else { + return; + }; + + // TODO ideally this would be a syntactic check that only matches on literal `...` + // in the source, rather than matching on the type. But this would require storing + // additional information in `EnumMetadata`. + let is_ellipsis = matches!( + member_value_type, + Type::NominalInstance(nominal_instance) + if nominal_instance.has_known_class(db, KnownClass::EllipsisType) + ); + // `auto()` values are computed at runtime by the enum metaclass, + // so we can't validate them against _value_ or __init__ at the type level. + let is_auto = enum_info.auto_members.contains(&member.name); + let skip_type_check = (context.in_stub() && is_ellipsis) || is_auto; + + if !skip_type_check { + if let Some(init_function) = enum_info.init_function { + check_enum_member_against_init( + context, + init_function, + instance_of_class, + member_value_type, + &member.name, + *first_reachable_definition, + ); + } else if let Some(expected_type) = enum_info.value_annotation { + if !member_value_type.is_assignable_to(db, expected_type) { + if let Some(builder) = context.report_lint( + &INVALID_ASSIGNMENT, + first_reachable_definition.focus_range(db, context.module()), + ) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Enum member `{}` value is not assignable to expected type", + &member.name + )); + diagnostic.info(format_args!( + "Expected `{}`, got `{}`", + expected_type.display(db), + member_value_type.display(db) + )); + } + } + } + } + } + } + let mut subclass_overrides_superclass_declaration = false; let mut has_dynamic_superclass = false; let mut has_typeddict_in_mro = false; @@ -455,6 +528,7 @@ bitflags! { const PROHIBITED_NAMED_TUPLE_ATTR = 1 << 3; const INVALID_DATACLASS = 1 << 4; const FINAL_VARIABLE_OVERRIDDEN = 1 << 5; + const INVALID_ENUM_VALUE = 1 << 6; } } @@ -483,6 +557,9 @@ impl From<&InferContext<'_, '_>> for OverrideRulesConfig { if rule_selection.is_enabled(LintId::of(&OVERRIDE_OF_FINAL_VARIABLE)) { config |= OverrideRulesConfig::FINAL_VARIABLE_OVERRIDDEN; } + if rule_selection.is_enabled(LintId::of(&INVALID_ASSIGNMENT)) { + config |= OverrideRulesConfig::INVALID_ENUM_VALUE; + } config } @@ -653,3 +730,61 @@ fn check_post_init_signature<'db>( as positional-only parameters", ); } + +/// Validates an enum member value against the enum's `__init__` signature. +/// +/// The enum metaclass unpacks tuple values as positional arguments to `__init__`, +/// and passes non-tuple values as a single argument. This function synthesizes +/// a call to `__init__` with the appropriate arguments and reports a diagnostic +/// if the call would fail. +fn check_enum_member_against_init<'db>( + context: &InferContext<'db, '_>, + init_function: FunctionType<'db>, + self_type: Type<'db>, + member_value_type: Type<'db>, + member_name: &Name, + definition: Definition<'db>, +) { + let db = context.db(); + + // The enum metaclass unpacks tuple values as positional args: + // MEMBER = (a, b, c) → __init__(self, a, b, c) + // MEMBER = x → __init__(self, x) + let args: Vec> = if let Type::NominalInstance(instance) = member_value_type { + if let Some(spec) = instance.tuple_spec(db) { + if let Tuple::Fixed(fixed) = &*spec { + fixed.all_elements().to_vec() + } else { + // Variable-length tuples: can't determine exact args, skip validation. + return; + } + } else { + vec![member_value_type] + } + } else { + vec![member_value_type] + }; + + let call_args = CallArguments::positional(args); + let call_args = call_args.with_self(Some(self_type)); + + let result = Type::FunctionLiteral(init_function) + .bindings(db) + .match_parameters(db, &call_args) + .check_types(db, &call_args, TypeContext::default(), &[]); + + if result.is_err() { + if let Some(builder) = context.report_lint( + &INVALID_ASSIGNMENT, + definition.focus_range(db, context.module()), + ) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Enum member `{member_name}` is incompatible with `__init__`", + )); + diagnostic.info(format_args!( + "Expected compatible arguments for `{}`", + Type::FunctionLiteral(init_function).display(db), + )); + } + } +} From 0e19fc9a61477e71abc4eb76f05a129b6b9ab873 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:26:13 +0900 Subject: [PATCH 097/261] [ty] defer calculating conjunctions in narrowing constraints (#23552) --- .../resources/mdtest/narrow/truthiness.md | 4 +- .../reachability_constraints.rs | 14 +- .../ty_python_semantic/src/types/builder.rs | 101 ++++++++++- crates/ty_python_semantic/src/types/narrow.rs | 168 +++++++++++------- 4 files changed, 208 insertions(+), 79 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index ff0c06e55ff00..5d52cb47daae4 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -31,14 +31,14 @@ else: reveal_type(x) # revealed: Never if x or not x: - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] + reveal_type(x) # revealed: Literal[-1, 0, "foo", "", b"bar", b""] | bool | None | tuple[()] else: reveal_type(x) # revealed: Never if not (x or not x): reveal_type(x) # revealed: Never else: - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] + reveal_type(x) # revealed: Literal[-1, 0, "foo", "", b"bar", b""] | bool | None | tuple[()] if (isinstance(x, int) or isinstance(x, str)) and x: reveal_type(x) # revealed: Literal[-1, True, "foo"] diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 63be1e85d72e2..a678ec1930efc 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -767,12 +767,11 @@ impl ReachabilityConstraintsBuilder { /// AND a new optional narrowing constraint with an accumulated one. fn accumulate_constraint<'db>( - db: &'db dyn Db, accumulated: Option>, new: Option>, ) -> Option> { match (accumulated, new) { - (Some(acc), Some(new_c)) => Some(new_c.merge_constraint_and(acc, db)), + (Some(acc), Some(new_c)) => Some(new_c.merge_constraint_and(acc)), (None, Some(new_c)) => Some(new_c), (Some(acc), None) => Some(acc), (None, None) => None, @@ -839,7 +838,7 @@ impl ReachabilityConstraints { // Apply all accumulated narrowing constraints to the base type match accumulated { Some(constraint) => NarrowingConstraint::intersection(base_ty) - .merge_constraint_and(constraint, db) + .merge_constraint_and(constraint) .evaluate_constraint_type(db), None => base_ty, } @@ -888,7 +887,7 @@ impl ReachabilityConstraints { is_positive: !predicate.is_positive, }; let neg_constraint = infer_narrowing_constraint(db, neg_predicate, place); - let false_accumulated = accumulate_constraint(db, accumulated, neg_constraint); + let false_accumulated = accumulate_constraint(accumulated, neg_constraint); return self.narrow_by_constraint_inner( db, predicates, @@ -901,7 +900,7 @@ impl ReachabilityConstraints { // If the false branch is statically unreachable, skip it entirely. if node.if_false == ALWAYS_FALSE { - let true_accumulated = accumulate_constraint(db, accumulated, pos_constraint); + let true_accumulated = accumulate_constraint(accumulated, pos_constraint); return self.narrow_by_constraint_inner( db, predicates, @@ -913,8 +912,7 @@ impl ReachabilityConstraints { } // True branch: predicate holds → accumulate positive narrowing - let true_accumulated = - accumulate_constraint(db, accumulated.clone(), pos_constraint); + let true_accumulated = accumulate_constraint(accumulated.clone(), pos_constraint); let true_ty = self.narrow_by_constraint_inner( db, predicates, @@ -930,7 +928,7 @@ impl ReachabilityConstraints { is_positive: !predicate.is_positive, }; let neg_constraint = infer_narrowing_constraint(db, neg_predicate, place); - let false_accumulated = accumulate_constraint(db, accumulated, neg_constraint); + let false_accumulated = accumulate_constraint(accumulated, neg_constraint); let false_ty = self.narrow_by_constraint_inner( db, predicates, diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 58dba0bf97343..3ae43a51d9cd2 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -45,6 +45,87 @@ use crate::types::{ use crate::{Db, FxOrderMap, FxOrderSet}; use smallvec::SmallVec; +/// Extract `(core, guard)` from truthiness-guarded intersections. +/// +/// e.g. +/// - `A & ~AlwaysTruthy` -> `Some((A, ~AlwaysTruthy))` +/// - `A & ~AlwaysFalsy` -> `Some((A, ~AlwaysFalsy))` +/// - `A` -> `None` +/// - `A & ~AlwaysTruthy & ~AlwaysFalsy` -> `None` (not a single-guard shape) +/// +/// This only recognizes the "single truthiness guard" forms used by truthiness narrowing. +fn split_truthiness_guarded_intersection<'db>( + db: &'db dyn Db, + ty: Type<'db>, +) -> Option<(Type<'db>, Type<'db>)> { + let Type::Intersection(intersection) = ty else { + return None; + }; + let falsy = Type::AlwaysTruthy.negate(db); + let truthy = Type::AlwaysFalsy.negate(db); + + let has_not_truthy = intersection.negative(db).contains(&Type::AlwaysTruthy); + let has_not_falsy = intersection.negative(db).contains(&Type::AlwaysFalsy); + let guard = match (has_not_truthy, has_not_falsy) { + (true, false) => falsy, + (false, true) => truthy, + _ => return None, + }; + + let mut core = IntersectionBuilder::new(db); + for positive in intersection.positive(db) { + core = core.add_positive(*positive); + } + for negative in intersection.negative(db) { + if (guard == falsy && *negative == Type::AlwaysTruthy) + || (guard == truthy && *negative == Type::AlwaysFalsy) + { + continue; + } + core = core.add_negative(*negative); + } + Some((core.build(), guard)) +} + +/// Try to merge a complementary guarded pair into an unguarded core. +/// +/// e.g. +/// - `(A & ~AlwaysTruthy, A & ~AlwaysFalsy)` -> `Some(A)` +/// - `(A & ~AlwaysTruthy, B & ~AlwaysFalsy)` -> `Some(A | B)` if reconstruction is exact +/// - `(A & ~AlwaysTruthy, C)` -> `None` +/// +/// Safety rule: +/// The candidate merge is accepted only if adding each original guard back reconstructs +/// exactly the original operands (`left` and `right`). +/// +/// TODO: This processing is specialized for `AlwaysTruthy/AlwaysFalsy`. +/// It would be nice to generalize this in the future. +/// Discussion: +fn merge_truthiness_guarded_pair<'db>( + db: &'db dyn Db, + left: Type<'db>, + right: Type<'db>, +) -> Option> { + let (left_core, left_guard) = split_truthiness_guarded_intersection(db, left)?; + let (right_core, right_guard) = split_truthiness_guarded_intersection(db, right)?; + if left_guard == right_guard { + return None; + } + + if left_core.is_equivalent_to(db, right_core) { + return Some(left_core); + } + + let candidate = UnionType::from_elements(db, [left_core, right_core]); + let left_reconstructed = IntersectionType::from_two_elements(db, candidate, left_guard); + let right_reconstructed = IntersectionType::from_two_elements(db, candidate, right_guard); + if left_reconstructed == left && right_reconstructed == right { + Some(candidate) + } else { + None + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum LiteralKind<'db> { Int, @@ -650,10 +731,13 @@ impl<'db> UnionBuilder<'db> { } fn push_type(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { - let bool_pair = if let Some(LiteralValueTypeKind::Bool(b)) = ty.as_literal_value_kind() { - Some(LiteralValueTypeKind::Bool(!b)) - } else { - None + let mut ty = ty; + let bool_pair = |ty: Type<'db>| { + if let Some(LiteralValueTypeKind::Bool(b)) = ty.as_literal_value_kind() { + Some(LiteralValueTypeKind::Bool(!b)) + } else { + None + } }; // If an alias gets here, it means we aren't unpacking aliases, and we also @@ -686,9 +770,16 @@ impl<'db> UnionBuilder<'db> { return; } + // Fold `(T & ~AlwaysTruthy) | (T & ~AlwaysFalsy)` to `T`. + if let Some(merged_type) = merge_truthiness_guarded_pair(self.db, ty, element_type) { + to_remove.push(i); + ty = merged_type; + continue; + } + if element_type .as_literal_value_kind() - .zip(bool_pair) + .zip(bool_pair(ty)) .is_some_and(|(element, pair)| element == pair) { self.add_in_place_impl(KnownClass::Bool.to_instance(self.db), seen_aliases); diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 0aca7c4299bcd..338eb2cb6e06e 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -271,6 +271,44 @@ impl ClassInfoConstraintFunction { } } +#[derive(Hash, PartialEq, Debug, Eq, Clone, salsa::Update, get_size2::GetSize)] +struct Conjunctions<'db> { + conjuncts: SmallVec<[Type<'db>; 2]>, +} + +impl<'db> Conjunctions<'db> { + fn singleton(ty: Type<'db>) -> Self { + Self { + conjuncts: smallvec![ty], + } + } + + fn and_with(mut self, other: Self) -> Self { + if self.conjuncts.iter().any(Type::is_never) || other.conjuncts.iter().any(Type::is_never) { + return Self::singleton(Type::Never); + } + + for conjunct in other.conjuncts { + if !self.conjuncts.contains(&conjunct) { + self.conjuncts.push(conjunct); + } + } + self + } + + fn evaluate_constraint_type(self, db: &'db dyn Db) -> Type<'db> { + if self.conjuncts.len() == 1 { + return self.conjuncts[0]; + } + + let mut intersection = IntersectionBuilder::new(db); + for conjunct in self.conjuncts { + intersection = intersection.add_positive(conjunct); + } + intersection.build() + } +} + /// Represents narrowing constraints in Disjunctive Normal Form (DNF). /// /// This is a disjunction (OR) of conjunctions (AND) of constraints. @@ -280,30 +318,30 @@ impl ClassInfoConstraintFunction { /// For example: /// - `f(x) and g(x)` where f returns `TypeIs[A]` and g returns `TypeGuard[B]` /// => and -/// ===> `NarrowingConstraint { intersection_disjunct: Some(A), replacement_disjuncts: [] }` -/// ===> `NarrowingConstraint { intersection_disjunct: None, replacement_disjuncts: [B] }` -/// => `NarrowingConstraint { intersection_disjunct: None, replacement_disjuncts: [B] }` +/// ===> `NarrowingConstraint { intersection_disjuncts: [A], replacement_disjuncts: [] }` +/// ===> `NarrowingConstraint { intersection_disjuncts: [], replacement_disjuncts: [B] }` +/// => `NarrowingConstraint { intersection_disjuncts: [], replacement_disjuncts: [B] }` /// => evaluates to `B` (`TypeGuard` clobbers any previous type information) /// /// - `f(x) or g(x)` where f returns `TypeIs[A]` and g returns `TypeGuard[B]` /// => or -/// ===> `NarrowingConstraint { intersection_disjunct: Some(A), replacement_disjuncts: [] }` -/// ===> `NarrowingConstraint { intersection_disjunct: None, replacement_disjuncts: [B] }` -/// => `NarrowingConstraint { intersection_disjunct: Some(A), replacement_disjuncts: [B] }` +/// ===> `NarrowingConstraint { intersection_disjuncts: [A], replacement_disjuncts: [] }` +/// ===> `NarrowingConstraint { intersection_disjuncts: [], replacement_disjuncts: [B] }` +/// => `NarrowingConstraint { intersection_disjuncts: [A], replacement_disjuncts: [B] }` /// => evaluates to `(P & A) | B`, where `P` is our previously-known type #[derive(Hash, PartialEq, Debug, Eq, Clone, salsa::Update, get_size2::GetSize)] pub(crate) struct NarrowingConstraint<'db> { /// Intersection constraint (from `isinstance()` narrowing comparisons, `TypeIs`, and - /// similar). We can use a single type here because we can eagerly union disjunctions - /// and eagerly intersect conjunctions. - intersection_disjunct: Option>, + /// similar). We keep these as a disjunction of conjunctions to avoid constructing + /// union/intersection types while merging constraints. + intersection_disjuncts: SmallVec<[Conjunctions<'db>; 1]>, /// "Replacement" constraints: instead of intersecting the previous type with a new type, /// the previous type is simply replaced wholesale with the new type. A common use case for /// these constraints is `typing.TypeGuard`. We can't eagerly union disjunctions because /// `TypeGuard` clobbers the previously-known type; within each replacement disjunct, however, /// we may eagerly intersect conjunctions with a later intersection narrowing. - replacement_disjuncts: SmallVec<[Type<'db>; 1]>, + replacement_disjuncts: SmallVec<[Conjunctions<'db>; 1]>, } impl<'db> NarrowingConstraint<'db> { @@ -311,7 +349,7 @@ impl<'db> NarrowingConstraint<'db> { /// intersected with this constraint pub(crate) fn intersection(constraint: Type<'db>) -> Self { Self { - intersection_disjunct: Some(constraint), + intersection_disjuncts: smallvec_inline![Conjunctions::singleton(constraint)], replacement_disjuncts: smallvec![], } } @@ -320,51 +358,60 @@ impl<'db> NarrowingConstraint<'db> { /// replaced wholesale with this constraint fn replacement(constraint: Type<'db>) -> Self { Self { - intersection_disjunct: None, - replacement_disjuncts: smallvec_inline![constraint], + intersection_disjuncts: smallvec![], + replacement_disjuncts: smallvec_inline![Conjunctions::singleton(constraint)], } } /// Merge two constraints, taking their intersection but respecting "replacement" semantics (with /// `other` winning) - pub(crate) fn merge_constraint_and(&self, other: Self, db: &'db dyn Db) -> Self { + pub(crate) fn merge_constraint_and(&self, other: Self) -> Self { // Distribute AND over OR: (A1 | A2 | ...) AND (B1 | B2 | ...) // becomes (A1 & B1) | (A1 & B2) | ... | (A2 & B1) | ... // // In our representation, the RHS `replacement_disjuncts` will all clobber the LHS disjuncts // when they are `and`ed, so they'll just stay as is. // - // The thing we actually need to deal with is the RHS `intersection_disjunct`. It gets - // intersected with the LHS `intersection_disjunct` to form the new `intersection_disjunct`, - // and intersected with each LHS `replacement_disjunct` to form new additional - // `replacement_disjuncts`. - let Some(other_intersection_disjunct) = other.intersection_disjunct else { + // The thing we actually need to deal with is the RHS `intersection_disjuncts`. Each RHS + // disjunct gets intersected with each LHS disjunct, producing the cartesian product. + // This is still deferred as conjunction lists. + // + // We also intersect each LHS `replacement_disjunct` with every RHS intersection disjunct + // to form new additional `replacement_disjuncts`. + if other.intersection_disjuncts.is_empty() { return other; - }; + } - let new_intersection_disjunct = self.intersection_disjunct.map(|intersection_disjunct| { - IntersectionType::from_elements( - db, - [intersection_disjunct, other_intersection_disjunct], - ) - }); + let mut new_intersection_disjuncts = smallvec![]; + for intersection_disjunct in &self.intersection_disjuncts { + for other_intersection_disjunct in &other.intersection_disjuncts { + let merged = intersection_disjunct + .clone() + .and_with(other_intersection_disjunct.clone()); + if !new_intersection_disjuncts.contains(&merged) { + new_intersection_disjuncts.push(merged); + } + } + } - let additional_replacement_disjuncts = - self.replacement_disjuncts - .iter() - .map(|replacement_disjunct| { - IntersectionType::from_elements( - db, - [*replacement_disjunct, other_intersection_disjunct], - ) - }); + let mut additional_replacement_disjuncts: SmallVec<[Conjunctions<'db>; 1]> = smallvec![]; + for replacement_disjunct in &self.replacement_disjuncts { + for other_intersection_disjunct in &other.intersection_disjuncts { + let merged = replacement_disjunct + .clone() + .and_with(other_intersection_disjunct.clone()); + if !additional_replacement_disjuncts.contains(&merged) { + additional_replacement_disjuncts.push(merged); + } + } + } let mut new_replacement_disjuncts = other.replacement_disjuncts; new_replacement_disjuncts.extend(additional_replacement_disjuncts); NarrowingConstraint { - intersection_disjunct: new_intersection_disjunct, + intersection_disjuncts: new_intersection_disjuncts, replacement_disjuncts: new_replacement_disjuncts, } } @@ -373,12 +420,15 @@ impl<'db> NarrowingConstraint<'db> { /// /// Forgets whether each constraint originated from a `replacement` disjunct or not pub(crate) fn evaluate_constraint_type(self, db: &'db dyn Db) -> Type<'db> { - UnionType::from_elements( - db, - self.replacement_disjuncts - .into_iter() - .chain(self.intersection_disjunct), - ) + let mut union = UnionBuilder::new(db); + for conjunctions in self + .replacement_disjuncts + .into_iter() + .chain(self.intersection_disjuncts) + { + union = union.add(conjunctions.evaluate_constraint_type(db)); + } + union.build() } } @@ -402,14 +452,13 @@ type NarrowingConstraints<'db> = FxHashMap( into: &mut NarrowingConstraints<'db>, from: NarrowingConstraints<'db>, - db: &'db dyn Db, ) { for (key, from_constraint) in from { match into.entry(key) { Entry::Occupied(mut entry) => { let into_constraint = entry.get(); - entry.insert(into_constraint.merge_constraint_and(from_constraint, db)); + entry.insert(into_constraint.merge_constraint_and(from_constraint)); } Entry::Vacant(entry) => { entry.insert(from_constraint); @@ -428,7 +477,6 @@ fn merge_constraints_and<'db>( fn merge_constraints_or<'db>( into: &mut NarrowingConstraints<'db>, from: NarrowingConstraints<'db>, - db: &'db dyn Db, ) { // For places that appear in `into` but not in `from`, widen to object into.retain(|key, _| from.contains_key(key)); @@ -437,16 +485,10 @@ fn merge_constraints_or<'db>( match into.entry(key) { Entry::Occupied(mut entry) => { let into_constraint = entry.get_mut(); - // Union the intersection constraints - into_constraint.intersection_disjunct = match ( - into_constraint.intersection_disjunct, - from_constraint.intersection_disjunct, - ) { - (Some(a), Some(b)) => Some(UnionType::from_two_elements(db, a, b)), - (Some(a), None) => Some(a), - (None, Some(b)) => Some(b), - (None, None) => None, - }; + // Union the intersection constraints by concatenating disjunct lists. + into_constraint + .intersection_disjuncts + .extend(from_constraint.intersection_disjuncts); // Concatenate replacement disjuncts into_constraint @@ -1174,7 +1216,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { constraints .entry(place) .and_modify(|existing| { - *existing = existing.merge_constraint_and(constraint.clone(), self.db); + *existing = existing.merge_constraint_and(constraint.clone()); }) .or_insert(constraint); } else if let Some((place, constraint)) = self.narrow_tuple_subscript( @@ -1187,7 +1229,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { constraints .entry(place) .and_modify(|existing| { - *existing = existing.merge_constraint_and(constraint.clone(), self.db); + *existing = existing.merge_constraint_and(constraint.clone()); }) .or_insert(constraint); } @@ -1349,7 +1391,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { constraints .entry(place) .and_modify(|existing| { - *existing = existing.merge_constraint_and(constraint.clone(), self.db); + *existing = existing.merge_constraint_and(constraint.clone()); }) .or_insert(constraint); } @@ -1371,7 +1413,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { constraints .entry(place) .and_modify(|existing| { - *existing = existing.merge_constraint_and(constraint.clone(), self.db); + *existing = existing.merge_constraint_and(constraint.clone()); }) .or_insert(constraint); @@ -1670,8 +1712,6 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { predicates: &Vec>, is_positive: bool, ) -> Option> { - let db = self.db; - // DeMorgan's law---if the overall `or` is negated, we need to `and` the negated sub-constraints. let merge_constraints = if is_positive { merge_constraints_or @@ -1685,7 +1725,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { self.evaluate_pattern_predicate_kind(predicate, subject, is_positive) }) .reduce(|mut constraints, constraints_| { - merge_constraints(&mut constraints, constraints_, db); + merge_constraints(&mut constraints, constraints_); constraints }) } @@ -1717,7 +1757,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { let mut aggregation: Option = None; for sub_constraint in sub_constraints.into_iter().flatten() { if let Some(ref mut some_aggregation) = aggregation { - merge_constraints_and(some_aggregation, sub_constraint, self.db); + merge_constraints_and(some_aggregation, sub_constraint); } else { aggregation = Some(sub_constraint); } @@ -1733,7 +1773,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { if let Some(ref mut first) = first { for rest_constraint in rest { if let Some(rest_constraint) = rest_constraint { - merge_constraints_or(first, rest_constraint, self.db); + merge_constraints_or(first, rest_constraint); } else { return None; } From e5f2f36a3f49b45fd7506d42b12c495c2517e936 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:22:43 -0500 Subject: [PATCH 098/261] Bump 0.15.3 (#23585) --- CHANGELOG.md | 83 +++++++++++++++++++ Cargo.lock | 6 +- README.md | 6 +- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- .../src/rules/pydocstyle/rules/sections.rs | 2 +- .../rules/swap_with_temporary_variable.rs | 2 +- .../rules/unnecessary_assign_before_yield.rs | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/formatter.md | 2 +- docs/integrations.md | 8 +- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 14 files changed, 103 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a9aa66538584..94d4b1759bbf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,88 @@ # Changelog +## 0.15.3 + +Released on 2026-02-26. + +### Preview features + +- Drop explicit support for `.qmd` file extension ([#23572](https://github.com/astral-sh/ruff/pull/23572)) + + This can now be enabled instead by setting the [`extension`](https://docs.astral.sh/ruff/settings/#extension) option: + + ```toml + # ruff.toml + extension = { qmd = "markdown" } + + # pyproject.toml + [tool.ruff] + extension = { qmd = "markdown" } + ``` + +- Include configured extensions in file discovery ([#23400](https://github.com/astral-sh/ruff/pull/23400)) + +- \[`flake8-bandit`\] Allow suspicious imports in `TYPE_CHECKING` blocks (`S401`-`S415`) ([#23441](https://github.com/astral-sh/ruff/pull/23441)) + +- \[`flake8-bugbear`\] Allow `B901` in pytest hook wrappers ([#21931](https://github.com/astral-sh/ruff/pull/21931)) + +- \[`flake8-import-conventions`\] Add missing conventions from upstream (`ICN001`, `ICN002`) ([#21373](https://github.com/astral-sh/ruff/pull/21373)) + +- \[`pydocstyle`\] Add rule to enforce docstring section ordering (`D420`) ([#23537](https://github.com/astral-sh/ruff/pull/23537)) + +- \[`pylint`\] Implement `swap-with-temporary-variable` (`PLR1712`) ([#22205](https://github.com/astral-sh/ruff/pull/22205)) + +- \[`ruff`\] Add `unnecessary-assign-before-yield` (`RUF070`) ([#23300](https://github.com/astral-sh/ruff/pull/23300)) + +- \[`ruff`\] Support file-level noqa in `RUF102` ([#23535](https://github.com/astral-sh/ruff/pull/23535)) + +- \[`ruff`\] Suppress diagnostic for invalid f-strings before Python 3.12 (`RUF027`) ([#23480](https://github.com/astral-sh/ruff/pull/23480)) + +- \[`flake8-bandit`\] Don't flag `BaseLoader`/`CBaseLoader` as unsafe (`S506`) ([#23510](https://github.com/astral-sh/ruff/pull/23510)) + +### Bug fixes + +- Avoid infinite loop between `I002` and `PYI025` ([#23352](https://github.com/astral-sh/ruff/pull/23352)) +- \[`pyflakes`\] Fix false positive for `@overload` from `lint.typing-modules` (`F811`) ([#23357](https://github.com/astral-sh/ruff/pull/23357)) +- \[`pyupgrade`\] Fix false positive for `TypeVar` default before Python 3.12 (`UP046`) ([#23540](https://github.com/astral-sh/ruff/pull/23540)) +- \[`pyupgrade`\] Fix handling of `\N` in raw strings (`UP032`) ([#22149](https://github.com/astral-sh/ruff/pull/22149)) + +### Rule changes + +- Render sub-diagnostics in the GitHub output format ([#23455](https://github.com/astral-sh/ruff/pull/23455)) + +- \[`flake8-bugbear`\] Tag certain `B007` diagnostics as unnecessary ([#23453](https://github.com/astral-sh/ruff/pull/23453)) + +- \[`ruff`\] Ignore unknown rule codes in `RUF100` ([#23531](https://github.com/astral-sh/ruff/pull/23531)) + + These are now flagged by [`RUF102`](https://docs.astral.sh/ruff/rules/invalid-rule-code/) instead. + +### Documentation + +- Fix missing settings links for several linters ([#23519](https://github.com/astral-sh/ruff/pull/23519)) +- Update isort action comments heading ([#23515](https://github.com/astral-sh/ruff/pull/23515)) +- \[`pydocstyle`\] Fix double comma in description of `D404` ([#23440](https://github.com/astral-sh/ruff/pull/23440)) + +### Other changes + +- Update the Python module (notably `find_ruff_bin`) for parity with uv ([#23406](https://github.com/astral-sh/ruff/pull/23406)) + +### Contributors + +- [@zanieb](https://github.com/zanieb) +- [@o1x3](https://github.com/o1x3) +- [@assadyousuf](https://github.com/assadyousuf) +- [@kar-ganap](https://github.com/kar-ganap) +- [@denyszhak](https://github.com/denyszhak) +- [@amyreese](https://github.com/amyreese) +- [@carljm](https://github.com/carljm) +- [@anishgirianish](https://github.com/anishgirianish) +- [@Bnyro](https://github.com/Bnyro) +- [@danparizher](https://github.com/danparizher) +- [@ntBre](https://github.com/ntBre) +- [@gcomneno](https://github.com/gcomneno) +- [@jaap3](https://github.com/jaap3) +- [@stakeswky](https://github.com/stakeswky) + ## 0.15.2 Released on 2026-02-19. diff --git a/Cargo.lock b/Cargo.lock index 14b38eedf8394..47f80fd269b8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3026,7 +3026,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.15.2" +version = "0.15.3" dependencies = [ "anyhow", "argfile", @@ -3289,7 +3289,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.2" +version = "0.15.3" dependencies = [ "aho-corasick", "anyhow", @@ -3663,7 +3663,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.15.2" +version = "0.15.3" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index 01512ea4315f6..a17bf9f0cd1a5 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.15.2/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.15.2/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.15.3/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.15.3/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -186,7 +186,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.2 + rev: v0.15.3 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 8320cb989f884..4d4fb9c633409 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.15.2" +version = "0.15.3" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 6b6dd89200a2a..c610dfa633426 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.15.2" +version = "0.15.3" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 88d1489f81333..255fadd91bd57 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1428,7 +1428,7 @@ impl AlwaysFixableViolation for BlankLinesBetweenHeaderAndContent { /// - [NumPy docstring standard](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(preview_since = "0.15.3")] pub(crate) struct IncorrectSectionOrder { current: String, previous: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/swap_with_temporary_variable.rs b/crates/ruff_linter/src/rules/pylint/rules/swap_with_temporary_variable.rs index 01df57ac71b2b..0b8aa70d3d389 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/swap_with_temporary_variable.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/swap_with_temporary_variable.rs @@ -39,7 +39,7 @@ use crate::checkers::ast::Checker; /// The rule's fix is marked as safe, unless the replacement range contains comments /// that would be removed. #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(preview_since = "0.15.3")] pub(crate) struct SwapWithTemporaryVariable<'a> { first: &'a Name, second: &'a Name, diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_assign_before_yield.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_assign_before_yield.rs index b968d6fafcad4..3e2e95245555e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_assign_before_yield.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_assign_before_yield.rs @@ -40,7 +40,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// variable assignment changes the local variable bindings visible to /// `locals()` and debuggers when the generator is suspended at the `yield`. #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(preview_since = "0.15.3")] pub(crate) struct UnnecessaryAssignBeforeYield { name: String, is_yield_from: bool, diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 5a67f2ee8d2b8..be4ca1ddd0cca 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.15.2" +version = "0.15.3" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index d755f3b57990e..4d07a52cf58b8 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -328,7 +328,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.2 + rev: v0.15.3 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/integrations.md b/docs/integrations.md index 04685ca84b44e..b2e0f45ae0319 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.15.2-alpine + name: ghcr.io/astral-sh/ruff:0.15.3-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.2 + rev: v0.15.3 hooks: # Run the linter. - id: ruff-check @@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.2 + rev: v0.15.3 hooks: # Run the linter. - id: ruff-check @@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.2 + rev: v0.15.3 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 9f5ecab1b9175..99b539fb8aad1 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.2 + rev: v0.15.3 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index e266125471b41..68efc9e3ad49e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.15.2" +version = "0.15.3" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 5f7f9a1919700..6b69e82c34cfe 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.15.2" +version = "0.15.3" description = "" authors = ["Charles Marsh "] From a62ba8c6e2bac0b899d90fd30a1b26c07aac44bb Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Feb 2026 17:16:56 +0100 Subject: [PATCH 099/261] [ty] Fix overloaded callable assignability for unary Callable targets (#23277) ## Summary Fixes https://github.com/astral-sh/ty/issues/2546 Improves assignability checking from overloaded callables to a single `Callable[...]` target with an explicit union parameter domain. Previously, this was handled with per-overload `when_any` checks. This PR replaces that with an aggregate probe over the overload set that: - filters down to overlapping overload arms, - unions their parameter domains and return types, - checks parameter coverage and return compatibility against the target callable. The aggregate probe is accept-only ie. if it isn't definitively satisfied, we fall back to the existing `when_any` behavior. This change is intentionally scoped to unary targets with explicit union domains and excludes dynamic/typevar candidates. General `n > 1` overload-set assignability is a way larger problem left for later. ## Test Plan - Added/updated mdtests for explicit [ #2546 ](https://github.com/astral-sh/ty/issues/2546) repros and negative cases (missing domain coverage, incompatible return union) in legacy generic-callables. - Added mdtests for overloaded generic-callable argument handling in legacy and PEP 695 callables, including `Callable[[T], T]` under union-constrained inference. - Added dataclass-transform converter coverage (`overloaded_converter`, `ConverterClass`) with an explicit TODO expectation for the still unhandled `converter=dict` case. - Added a reduced SymPy one-import MRE to lock the overload/protocol panic shape. - Updated Liskov tests for unannotated overrides of overloaded dunder methods. --------- Co-authored-by: Douglas Creager --- .../resources/mdtest/annotations/callable.md | 94 +++++++++++ .../mdtest/dataclasses/dataclass_transform.md | 40 +++++ .../mdtest/generics/legacy/callables.md | 152 ++++++++++++++++++ .../mdtest/generics/pep695/callables.md | 139 ++++++++++++++++ .../resources/mdtest/liskov.md | 7 + ...s_hie\342\200\246_(5e8fca10d966c36e).snap" | 8 + crates/ty_python_semantic/src/types.rs | 4 + .../ty_python_semantic/src/types/relation.rs | 2 +- .../src/types/signatures.rs | 126 +++++++++++++-- 9 files changed, 562 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index 1fd1fcd24a2a5..e201182c9ed9b 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -187,6 +187,100 @@ def _(c: Callable[[int, str], int]): reveal_type(c) # revealed: (int, str, /) -> int ``` +## Overloaded callable assignability + +An overloaded callable should be assignable to a non-overloaded callable type when the overload set +as a whole is compatible with the target callable. + +```py +from typing import Callable, overload + +@overload +def foo(x: int) -> str: ... +@overload +def foo(x: str) -> str: ... +def foo(x: int | str) -> str: + return str(x) + +def expects(c: Callable[[int | str], str]) -> None: + pass + +expects(foo) +``` + +```py +from typing import overload + +@overload +def foo(x: int) -> str: ... +@overload +def foo(x: str) -> str: ... +def foo(x: int | str) -> str: + return str(x) + +def errors() -> None: + for x in map(foo, range(1, 10)): + print(x) +``` + +```py +from typing import Callable, overload + +@overload +def converter(x: int) -> str: ... +@overload +def converter(x: bytes) -> bytes: ... +def converter(x: int | bytes) -> str | bytes: + if isinstance(x, int): + return str(x) + return x + +def expects_int_str(c: Callable[[int], str]) -> None: + pass + +expects_int_str(converter) +``` + +The overload set must cover the full target parameter domain. + +```py +from typing import Callable, overload + +@overload +def partial_converter(x: int) -> str: ... +@overload +def partial_converter(x: bytes) -> str: ... +def partial_converter(x: int | bytes) -> str: + return str(x) + +def expects_int_or_str(c: Callable[[int | str], str]) -> None: + pass + +# error: [invalid-argument-type] +expects_int_or_str(partial_converter) +``` + +Even when the parameter domain is covered, return compatibility must still hold. + +```py +from typing import Callable, overload + +@overload +def wide_return_converter(x: int) -> str: ... +@overload +def wide_return_converter(x: str) -> bytes: ... +def wide_return_converter(x: int | str) -> str | bytes: + if isinstance(x, int): + return str(x) + return x.encode() + +def expects_str_return(c: Callable[[int | str], str]) -> None: + pass + +# error: [invalid-argument-type] +expects_str_return(wide_return_converter) +``` + ## Union ```py diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md index 650dd838f5df6..07810c75f1db5 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -894,6 +894,46 @@ class Person: reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None ``` +### Converter field specifier with overloaded callables + +```py +from typing import Callable, TypeVar, overload +from typing_extensions import dataclass_transform + +T = TypeVar("T") +S = TypeVar("S") + +def model_field(*, converter: Callable[[S], T], default: S | None = None) -> T: + raise NotImplementedError + +@dataclass_transform(field_specifiers=(model_field,)) +class ModelBase: ... + +@overload +def overloaded_converter(s: str) -> int: ... +@overload +def overloaded_converter(s: list[str]) -> int: ... +def overloaded_converter(s: str | list[str], *args: str) -> int | str: + return 0 + +class ConverterClass: + @overload + def __init__(self, val: str) -> None: ... + @overload + def __init__(self, val: bytes) -> None: ... + def __init__(self, val: str | bytes) -> None: + pass + +class Model(ModelBase): + field3: ConverterClass = model_field(converter=ConverterClass) + field4: int = model_field(converter=overloaded_converter) + # TODO: This should be accepted once overloaded class callables with richer signatures are + # modeled in callable assignability. + # error: [invalid-assignment] + # error: [invalid-argument-type] + field5: dict[str, str] = model_field(converter=dict, default=()) +``` + ### Nested dataclass-transformers Make sure that models are only affected by the field specifiers of their own transformer: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md index 2420744938510..b305eeb333cbe 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md @@ -245,3 +245,155 @@ reveal_type(generic_context(outside_callable(int_identity))) # error: [invalid-argument-type] outside_callable(int_identity)("string") ``` + +## Overloaded callable as generic `Callable` argument + +An overloaded callable should be assignable to a non-overloaded callable type when the overload set +as a whole is compatible with the target callable. + +The type variable should be inferred from the first matching overload, rather than unioning +parameter types across all overloads (which would create an unsatisfiable expected type for +contravariant type variables). + +```py +from typing import Callable, TypeVar, overload + +T = TypeVar("T") + +def accepts_callable(converter: Callable[[T], None]) -> T: + raise NotImplementedError + +@overload +def f(val: str) -> None: ... +@overload +def f(val: bytes) -> None: ... +def f(val: str | bytes) -> None: + pass + +reveal_type(accepts_callable(f)) # revealed: str | bytes +``` + +When `T` is constrained to a union by other arguments, the overloaded callable must still be treated +as a whole to satisfy `Callable[[T], T]`. + +```py +from typing import Callable, TypeVar, overload + +T = TypeVar("T") + +def apply_twice(converter: Callable[[T], T], left: T, right: T) -> tuple[T, T]: + return converter(left), converter(right) + +@overload +def f(val: int) -> int: ... +@overload +def f(val: str) -> str: ... +def f(val: int | str) -> int | str: + return val + +x: int | str = 1 +y: int | str = "a" + +result = apply_twice(f, x, y) +# revealed: tuple[int | str, int | str] +reveal_type(result) +``` + +An overloaded callable returned from a generic callable factory should still be assignable to the +declared generic callable return type. + +```py +from collections.abc import Callable, Coroutine +from typing import Any, TypeVar, overload + +S = TypeVar("S") +T = TypeVar("T") +U = TypeVar("U") + +def singleton(flag: bool = False) -> Callable[[Callable[[int], S]], Callable[[int], S]]: + @overload + def wrapper(func: Callable[[int], Coroutine[Any, Any, T]]) -> Callable[[int], Coroutine[Any, Any, T]]: ... + @overload + def wrapper(func: Callable[[int], U]) -> Callable[[int], U]: ... + def wrapper(func: Callable[[int], Coroutine[Any, Any, T] | U]) -> Callable[[int], Coroutine[Any, Any, T] | U]: + return func + + return wrapper +``` + +## SymPy one-import MRE scaffold (multi-file) + +Reduced regression lock for a SymPy overload/protocol shape that can panic in the +overload-assignability path. + +```py +from __future__ import annotations + +from sympy.polys.compatibility import Domain, IPolys +from typing import Generic, TypeVar, overload + +T = TypeVar("T") + +class DefaultPrinting: + pass + +class PolyRing(DefaultPrinting, IPolys[T], Generic[T]): + symbols: tuple[object, ...] + domain: Domain[T] + + def clone( + self, + symbols: object | None = None, + domain: object | None = None, + order: object | None = None, + ) -> PolyRing[T]: + return self + + @overload + def __getitem__(self, key: int) -> PolyRing[T]: ... + @overload + def __getitem__(self, key: slice) -> PolyRing[T] | Domain[T]: ... + def __getitem__(self, key: slice | int) -> PolyRing[T] | Domain[T]: + symbols = self.symbols[key] + if not symbols: + return self.domain + return self.clone(symbols=symbols) + +def takes_ring(x: PolyRing[int]) -> None: + reveal_type(x[0]) # revealed: PolyRing[int] + reveal_type(x[:]) # revealed: PolyRing[int] | Domain[int] +``` + +`sympy/polys/compatibility.pyi`: + +```pyi +from __future__ import annotations + +from typing import Generic, Protocol, TypeVar, overload + +T = TypeVar("T") +S = TypeVar("S") + +class Domain(Generic[T]): ... + +class IPolys(Protocol[T]): + @overload + def clone( + self, + symbols: object | None = None, + domain: None = None, + order: None = None, + ) -> IPolys[T]: ... + @overload + def clone( + self, + symbols: object | None = None, + *, + domain: Domain[S], + order: None = None, + ) -> IPolys[S]: ... + @overload + def __getitem__(self, key: int) -> IPolys[T]: ... + @overload + def __getitem__(self, key: slice) -> IPolys[T] | Domain[T]: ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/callables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/callables.md index 199fa22ea832c..1573d68aebec9 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/callables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/callables.md @@ -242,3 +242,142 @@ reveal_type(generic_context(outside_callable(int_identity))) # error: [invalid-argument-type] outside_callable(int_identity)("string") ``` + +## Overloaded callable as generic `Callable` argument + +An overloaded callable should be assignable to a non-overloaded callable type when the overload set +as a whole is compatible with the target callable. + +The type variable should be inferred from the first matching overload, rather than unioning +parameter types across all overloads (which would create an unsatisfiable expected type for +contravariant type variables). + +```py +from typing import Callable, overload + +def accepts_callable[T](converter: Callable[[T], None]) -> T: + raise NotImplementedError + +@overload +def f(val: str) -> None: ... +@overload +def f(val: bytes) -> None: ... +def f(val: str | bytes) -> None: + pass + +reveal_type(accepts_callable(f)) # revealed: str | bytes +``` + +When `T` is constrained to a union by other arguments, the overloaded callable must still be treated +as a whole to satisfy `Callable[[T], T]`. + +```py +from typing import Callable, overload + +def apply_twice[T](converter: Callable[[T], T], left: T, right: T) -> tuple[T, T]: + return converter(left), converter(right) + +@overload +def f(val: int) -> int: ... +@overload +def f(val: str) -> str: ... +def f(val: int | str) -> int | str: + return val + +x: int | str = 1 +y: int | str = "a" + +result = apply_twice(f, x, y) +# revealed: tuple[int | str, int | str] +reveal_type(result) +``` + +An overloaded callable returned from a generic callable factory should still be assignable to the +declared generic callable return type. + +```py +from collections.abc import Callable, Coroutine +from typing import Any, overload + +def singleton[S](flag: bool = False) -> Callable[[Callable[[int], S]], Callable[[int], S]]: + @overload + def wrapper[T](func: Callable[[int], Coroutine[Any, Any, T]]) -> Callable[[int], Coroutine[Any, Any, T]]: ... + @overload + def wrapper[U](func: Callable[[int], U]) -> Callable[[int], U]: ... + def wrapper[T, U](func: Callable[[int], Coroutine[Any, Any, T] | U]) -> Callable[[int], Coroutine[Any, Any, T] | U]: + return func + + return wrapper +``` + +## SymPy one-import MRE scaffold (multi-file) + +Reduced regression lock for a SymPy overload/protocol shape that can panic in the +overload-assignability path. + +```py +from __future__ import annotations + +from sympy.polys.compatibility import Domain, IPolys +from typing import overload + +class DefaultPrinting: + pass + +class PolyRing[T](DefaultPrinting, IPolys[T]): + symbols: tuple[object, ...] + domain: Domain[T] + + def clone( + self, + symbols: object | None = None, + domain: object | None = None, + order: object | None = None, + ) -> PolyRing[T]: + return self + + @overload + def __getitem__(self, key: int) -> PolyRing[T]: ... + @overload + def __getitem__(self, key: slice) -> PolyRing[T] | Domain[T]: ... + def __getitem__(self, key: slice | int) -> PolyRing[T] | Domain[T]: + symbols = self.symbols[key] + if not symbols: + return self.domain + return self.clone(symbols=symbols) + +def takes_ring(x: PolyRing[int]) -> None: + reveal_type(x[0]) # revealed: PolyRing[int] + reveal_type(x[:]) # revealed: PolyRing[int] | Domain[int] +``` + +`sympy/polys/compatibility.pyi`: + +```pyi +from __future__ import annotations + +from typing import Protocol, overload + +class Domain[T]: ... + +class IPolys[T](Protocol): + @overload + def clone( + self, + symbols: object | None = None, + domain: None = None, + order: None = None, + ) -> IPolys[T]: ... + @overload + def clone[S]( + self, + symbols: object | None = None, + *, + domain: Domain[S], + order: None = None, + ) -> IPolys[S]: ... + @overload + def __getitem__(self, key: int) -> IPolys[T]: ... + @overload + def __getitem__(self, key: slice) -> IPolys[T] | Domain[T]: ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index 8bbe34c239958..913bb8ba57943 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -217,6 +217,13 @@ class D(C): def get(self, my_default): ... ``` +Unannotated overrides of overloaded dunder methods should remain accepted. + +```pyi +class C(list[int]): + def __getitem__(self, key): ... +``` + ## Non-generic methods on generic classes work as expected ```toml diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut\342\200\246_-_The_entire_class_hie\342\200\246_(5e8fca10d966c36e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut\342\200\246_-_The_entire_class_hie\342\200\246_(5e8fca10d966c36e).snap" index 619f0ba42ce2f..74ddba4e0bf4f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut\342\200\246_-_The_entire_class_hie\342\200\246_(5e8fca10d966c36e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut\342\200\246_-_The_entire_class_hie\342\200\246_(5e8fca10d966c36e).snap" @@ -1,5 +1,6 @@ --- source: crates/ty_test/src/lib.rs +assertion_line: 623 expression: snapshot --- @@ -79,6 +80,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md 17 | def get(self, my_default): ... ``` +## mdtest_snippet.pyi + +``` +1 | class C(list[int]): +2 | def __getitem__(self, key): ... +``` + # Diagnostics ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 2e6bb6dfccacc..b5132cec6306d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1185,6 +1185,10 @@ impl<'db> Type<'db> { }) } + pub(crate) fn has_dynamic(self, db: &'db dyn Db) -> bool { + any_over_type(db, self, false, |ty| ty.is_dynamic()) + } + pub(crate) fn has_typevar_or_typevar_instance(self, db: &'db dyn Db) -> bool { any_over_type(db, self, false, |ty| { matches!( diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 9edc5a27ed548..1fb575bf3c540 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -328,7 +328,7 @@ impl<'db> Type<'db> { is_redundant_with_impl(db, self, other) } - fn has_relation_to( + pub(super) fn has_relation_to( self, db: &'db dyn Db, target: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 5ece2a84f45e4..753c75533ebc0 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -25,7 +25,8 @@ use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelati use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypeKind, FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, ParamSpecAttrKind, SelfBinding, - TypeContext, TypeMapping, VarianceInferable, infer_complete_scope_types, todo_type, + TypeContext, TypeMapping, UnionBuilder, VarianceInferable, infer_complete_scope_types, + todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -341,6 +342,101 @@ impl<'db> CallableSignature<'db> { ) } + /// Fast path for unary callable assignability: compare overload sets by aggregating + /// overlapping parameter domains and return types. + /// + /// This is intentionally accept-only. If the probe does not definitely succeed, it returns + /// `None` and callers should fall back to legacy per-overload relation checks. + fn try_unary_overload_aggregate_relation( + db: &'db dyn Db, + self_signatures: &[Signature<'db>], + other_signature: &Signature<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation<'db>, + ) -> Option> { + let single_required_positional_parameter_type = |signature: &Signature<'db>| { + if signature.parameters().len() != 1 { + return None; + } + let parameter = signature.parameters().get(0)?; + + match parameter.kind() { + ParameterKind::PositionalOnly { + default_type: None, .. + } + | ParameterKind::PositionalOrKeyword { + default_type: None, .. + } => Some(parameter.annotated_type()), + _ => None, + } + }; + + let is_unary_overload_aggregate_candidate_type = |ty: Type<'db>| { + // Keep aggregate probing away from inference-sensitive shapes and defer them to the + // legacy path, which already handles dynamic/typevar interactions. + !ty.has_dynamic(db) && !ty.has_typevar_or_typevar_instance(db) + }; + + let other_parameter_type = single_required_positional_parameter_type(other_signature)?; + // Keep this aggregate path narrowly scoped to unary target callables whose parameter + // domain is an explicit union. + // + // Broader overload-set assignability (non-union unary domains, higher arity, + // typevars/dynamic interactions) needs dedicated relation logic. + if !matches!(other_parameter_type, Type::Union(_)) + || !is_unary_overload_aggregate_candidate_type(other_parameter_type) + || !is_unary_overload_aggregate_candidate_type(other_signature.return_ty) + { + return None; + } + + let mut parameter_type_union = UnionBuilder::new(db); + let mut return_type_union = UnionBuilder::new(db); + let mut has_overlapping_domain = false; + + for self_signature in self_signatures { + let self_parameter_type = single_required_positional_parameter_type(self_signature)?; + if !is_unary_overload_aggregate_candidate_type(self_parameter_type) + || !is_unary_overload_aggregate_candidate_type(self_signature.return_ty) + { + return None; + } + let signatures_are_disjoint = self_parameter_type + .when_disjoint_from(db, other_parameter_type, inferable) + .is_always_satisfied(db); + + if signatures_are_disjoint { + continue; + } + + has_overlapping_domain = true; + parameter_type_union = parameter_type_union.add(self_parameter_type); + return_type_union = return_type_union.add(self_signature.return_ty); + } + + if !has_overlapping_domain { + return None; + } + + // Function assignability here is parameter-contravariant and return-covariant. + let parameters_cover_target = other_parameter_type.has_relation_to( + db, + parameter_type_union.build(), + inferable, + relation, + ); + let returns_match_target = return_type_union.build().has_relation_to( + db, + other_signature.return_ty, + inferable, + relation, + ); + let aggregate_relation = parameters_cover_target.and(db, || returns_match_target); + aggregate_relation + .is_always_satisfied(db) + .then_some(aggregate_relation) + } + /// Implementation of subtyping and assignability between two, possible overloaded, callable /// types. fn has_relation_to_inner( @@ -473,17 +569,29 @@ impl<'db> CallableSignature<'db> { } // `self` is possibly overloaded while `other` is definitely not overloaded. - (_, [_]) => self_signatures.iter().when_any(db, |self_signature| { - Self::has_relation_to_inner( + (_, [other_signature]) => { + if let Some(aggregate_relation) = Self::try_unary_overload_aggregate_relation( db, - std::slice::from_ref(self_signature), - other_signatures, + self_signatures, + other_signature, inferable, relation, - relation_visitor, - disjointness_visitor, - ) - }), + ) { + return aggregate_relation; + } + + self_signatures.iter().when_any(db, |self_signature| { + Self::has_relation_to_inner( + db, + std::slice::from_ref(self_signature), + other_signatures, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } // `self` is definitely not overloaded while `other` is possibly overloaded. ([_], _) => other_signatures.iter().when_all(db, |other_signature| { From 5bc49a941279c9100ff834edae1cb63053aeec93 Mon Sep 17 00:00:00 2001 From: Dev-iL <6509619+Dev-iL@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:35:55 +0200 Subject: [PATCH 100/261] Increase the ruleset size to 16 bits (#23586) ## Summary There are currently 960 rules and no room for more. This raises the limit to 1024. ## Test Plan Ran the `configuration::tests::select_two_char_prefix` test after adding one extra rule. --- crates/ruff_linter/src/registry/rule_set.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/registry/rule_set.rs b/crates/ruff_linter/src/registry/rule_set.rs index 62601d7229a1c..de625379c83a0 100644 --- a/crates/ruff_linter/src/registry/rule_set.rs +++ b/crates/ruff_linter/src/registry/rule_set.rs @@ -5,7 +5,7 @@ use ruff_macros::CacheKey; use crate::registry::Rule; -const RULESET_SIZE: usize = 15; +const RULESET_SIZE: usize = 16; /// A set of [`Rule`]s. /// From fbb9fa75cc1915973a739da0ec469094176bfdc6 Mon Sep 17 00:00:00 2001 From: Goyabean <118635203+GeObts@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:36:17 -0600 Subject: [PATCH 101/261] docs: fix incorrect import-heading example (#23568) Fixes incorrect TOML example for import-heading configuration. Closes #23420 --------- Co-authored-by: GeObts Co-authored-by: Brent Westbrook --- crates/ruff_workspace/src/options.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index eb54e489b924e..0f8697445ec9d 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -2530,6 +2530,7 @@ pub struct IsortOptions { #[option( default = r#"{}"#, value_type = r#"dict["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str, str]"#, + scope = "import-heading", example = r#" future = "Future imports" standard-library = "Standard library imports" From 60facfa0bc02689637e25237f0df5abfc2f27054 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Thu, 26 Feb 2026 09:29:12 -0800 Subject: [PATCH 102/261] one word typo fix in a `while_loop.md` test case (#23589) --- crates/ty_python_semantic/resources/mdtest/loops/while_loop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md index e917582c5804a..e41803c2b7de8 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md @@ -518,7 +518,7 @@ a nested cycle, but we strip out _that_ `Divergent` in another part of cycle rec get the narrowing right and infer that `node` is of type `Node`, but then our monotonic widening step will union `Node` with `Node | None` from the previous iteration, reproduce the same wrong answer, and declare that to be the fixpoint. Finally we get false-positive warnings from the fact -that `Node` doesn't have a `.next` field. +that `None` doesn't have a `.next` field. So, because we do monotonic widening in cycle recovery, we need to make sure that temporarily `Divergent` expressions in narrowing constraints don't lead to too-wide-but-not-visibly-`Divergent` From 625b4f5a672d1baaa6f25e0999ca428f3f2522f1 Mon Sep 17 00:00:00 2001 From: Josh Cannon Date: Thu, 26 Feb 2026 12:38:39 -0600 Subject: [PATCH 103/261] [ruff] docs: Clarify first-party import detection in Ruff (#23591) --- docs/faq.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index b94f0b64e43e2..138bc87901d38 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -383,7 +383,8 @@ want to explicitly set the `src` option in the extended configuration file: ``` Beyond this `src`-based detection, Ruff will also attempt to determine the current Python package -for a given Python file, and mark imports from within the same package as first-party. For example, +for a given Python file (determined via the existence of a `__init__.py` file in a directory), +and mark imports from within the same package as first-party. For example, above, `baz.py` would be identified as part of the Python package beginning at `./my_project/src/foo`, and so any imports in `baz.py` that begin with `foo` (like `import foo.bar`) would be considered first-party based on this same-package heuristic. From 81d655fadce087b792e524ed1964e9bcc31b73cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E5=81=9A=E4=BA=86=E7=9D=A1=E5=A4=A7=E8=A7=89?= <64798754+stakeswky@users.noreply.github.com> Date: Fri, 27 Feb 2026 02:41:05 +0800 Subject: [PATCH 104/261] [`pyflakes`] suppress false positive in `F821` for names used before `del` in stub files (#23550) Co-authored-by: stakeswky Co-authored-by: Amethyst Reese --- .../test/fixtures/pyflakes/F821_34.pyi | 22 ++++++++++ crates/ruff_linter/src/rules/pyflakes/mod.rs | 1 + ...es__pyflakes__tests__F821_F821_34.pyi.snap | 21 +++++++++ crates/ruff_python_semantic/src/model.rs | 43 ++++++++++++++++++- 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F821_34.pyi create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.pyi.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_34.pyi b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_34.pyi new file mode 100644 index 0000000000000..72a4eceb470e6 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_34.pyi @@ -0,0 +1,22 @@ +# OK: `del` after use in stub file should not trigger F821 +class MyClass: ... +def f(cls: MyClass) -> None: ... +del MyClass + +# OK: same pattern with a variable +_T = int +x: _T +del _T + +# OK: used in base class +class Base: ... +class Child(Base): ... +del Base + +# Error: `del` before use should still trigger F821 +class EarlyDel: ... +del EarlyDel +def f2(cls: EarlyDel) -> None: ... # F821 + +# Error: name that was never defined +def g(x: Undefined) -> None: ... # F821 diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index da594769bc5f3..53d8958e58fa1 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -168,6 +168,7 @@ mod tests { #[test_case(Rule::UndefinedName, Path::new("F821_31.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_32.pyi"))] #[test_case(Rule::UndefinedName, Path::new("F821_33.py"))] + #[test_case(Rule::UndefinedName, Path::new("F821_34.pyi"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.pyi.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.pyi.snap new file mode 100644 index 0000000000000..25d450303719a --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.pyi.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- +F821 Undefined name `EarlyDel` + --> F821_34.pyi:19:13 + | +17 | class EarlyDel: ... +18 | del EarlyDel +19 | def f2(cls: EarlyDel) -> None: ... # F821 + | ^^^^^^^^ +20 | +21 | # Error: name that was never defined + | + +F821 Undefined name `Undefined` + --> F821_34.pyi:22:10 + | +21 | # Error: name that was never defined +22 | def g(x: Undefined) -> None: ... # F821 + | ^^^^^^^^^ + | diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index ec0749f7206c2..a64a413866263 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -502,7 +502,48 @@ impl<'a> SemanticModel<'a> { // print(x) // // The `x` in `print(x)` should be treated as unresolved. - BindingKind::Deletion | BindingKind::UnboundException(None) => { + BindingKind::Deletion => { + // In stub files, `del` is used to hide names from re-export + // while the name is still valid for use in type annotations + // within the same file. Since annotations are deferred in + // stubs, the `Deletion` binding is seen at resolve time even + // though the reference textually precedes the `del`. Resolve + // to the shadowed (pre-`del`) binding when available. + if self.in_stub_file() { + if let Some(shadowed_id) = + self.scopes[scope_id].shadowed_binding(binding_id) + { + // Only suppress F821 when the reference textually precedes + // the `del` statement. If `del` comes before the reference, + // the name is genuinely undefined at that point and F821 + // should still fire. + let deletion_range = self.bindings[binding_id].range; + if !self.bindings[shadowed_id].is_unbound() + && name.range.start() < deletion_range.start() + { + let reference_id = self.resolved_references.push( + self.scope_id, + self.node_id, + ExprContext::Load, + self.flags, + name.range, + ); + self.bindings[shadowed_id].references.push(reference_id); + self.resolved_names.insert(name.into(), shadowed_id); + return ReadResult::Resolved(shadowed_id); + } + } + } + + self.unresolved_references.push( + name.range, + self.exceptions(), + UnresolvedReferenceFlags::empty(), + ); + return ReadResult::UnboundLocal(binding_id); + } + + BindingKind::UnboundException(None) => { self.unresolved_references.push( name.range, self.exceptions(), From fd09d370076ab585444fd39f0fee79bf29280b68 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:19:32 -0500 Subject: [PATCH 105/261] Fix panic on access to definitions after analyzing definitions (#23588) Summary -- This PR fixes #23587 by removing this `std::mem::take` call: https://github.com/astral-sh/ruff/blob/a62ba8c6e2bac0b899d90fd30a1b26c07aac44bb/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs#L120 This was previously moving the `Definitions` out of the `Checker` and causing this operation to panic if the definitions were accessed in a later analysis phase: https://github.com/astral-sh/ruff/blob/a62ba8c6e2bac0b899d90fd30a1b26c07aac44bb/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs#L120 which was apparently never done before `PLR1712` was added. I don't see any reason why this really needed to be a move as it only took a couple of lifetimes to handle it with borrowing, so this seems like the easiest fix. I also changed the indexing operation to a `get` call (with a `debug_assert`) since the method already returns an `Option`, but doing that alone would prevent `PLR1712` from firing in the module scope. Test Plan -- A new test based on the issue that also covers the module scope mentioned above --- .../pylint/swap_with_temporary_variable_1.py | 8 +++++++ .../src/checkers/ast/analyze/definitions.rs | 7 +++++-- crates/ruff_linter/src/rules/pylint/mod.rs | 14 +++++++++++++ ...tests__conflict_with_definition_rules.snap | 21 +++++++++++++++++++ crates/ruff_python_semantic/src/definition.rs | 12 +++++++---- 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pylint/swap_with_temporary_variable_1.py create mode 100644 crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/swap_with_temporary_variable_1.py b/crates/ruff_linter/resources/test/fixtures/pylint/swap_with_temporary_variable_1.py new file mode 100644 index 0000000000000..caeb44828d59a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/swap_with_temporary_variable_1.py @@ -0,0 +1,8 @@ +""" +Test that PLR1712 fires in the module-global scope. +""" + +x, y = 1, 2 +temp = x # PLR1712 +x = y +y = temp diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index 68570cb27e1a8..7b259ac787dc0 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -117,12 +117,15 @@ pub(crate) fn definitions(checker: &mut Checker) { }) }; - let definitions = std::mem::take(&mut checker.semantic.definitions); let mut overloaded_name: Option<&str> = None; for ContextualizedDefinition { definition, visibility, - } in definitions.resolve(exports.as_deref()).iter() + } in checker + .semantic + .definitions + .resolve(exports.as_deref()) + .iter() { let docstring = docstrings::extraction::extract_docstring(definition); diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index c8ae94c246f37..ecb8fc3a61bc5 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -474,4 +474,18 @@ mod tests { assert_diagnostics!(diagnostics); Ok(()) } + + /// Regression test for . + #[test] + fn conflict_with_definition_rules() -> Result<()> { + let diagnostics = test_path( + Path::new("pylint/swap_with_temporary_variable_1.py"), + &LinterSettings::for_rules(vec![ + Rule::SwapWithTemporaryVariable, + Rule::MissingTypeFunctionArgument, + ]), + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap new file mode 100644 index 0000000000000..dfa9d8825dd7a --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +PLR1712 [*] Unnecessary temporary variable + --> swap_with_temporary_variable_1.py:6:1 + | +5 | x, y = 1, 2 +6 | / temp = x # PLR1712 +7 | | x = y +8 | | y = temp + | |________^ + | +help: Use `x, y = y, x` instead +3 | """ +4 | +5 | x, y = 1, 2 + - temp = x # PLR1712 + - x = y + - y = temp +6 + x, y = y, x +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_python_semantic/src/definition.rs b/crates/ruff_python_semantic/src/definition.rs index 7f6c724540264..7ff94cbed20ba 100644 --- a/crates/ruff_python_semantic/src/definition.rs +++ b/crates/ruff_python_semantic/src/definition.rs @@ -214,11 +214,11 @@ impl<'a> Definitions<'a> { } /// Resolve the visibility of each definition in the collection. - pub fn resolve(self, exports: Option<&[DunderAllName]>) -> ContextualizedDefinitions<'a> { + pub fn resolve(&'a self, exports: Option<&[DunderAllName]>) -> ContextualizedDefinitions<'a> { let mut definitions: IndexVec> = IndexVec::with_capacity(self.len()); - for definition in self { + for definition in self.iter() { // Determine the visibility of the next definition, taking into account its parent's // visibility. let visibility = { @@ -282,7 +282,11 @@ impl<'a> Definitions<'a> { /// Returns a reference to the Python AST. pub fn python_ast(&self) -> Option<&'a [Stmt]> { - let module = self[DefinitionId::module()].as_module()?; + let Some(definition) = self.get(DefinitionId::module()) else { + debug_assert!(false, "Module definition unavailable"); + return None; + }; + let module = definition.as_module()?; Some(module.python_ast) } } @@ -306,7 +310,7 @@ impl<'a> IntoIterator for Definitions<'a> { /// A [`Definition`] in a Python program with its resolved [`Visibility`]. pub struct ContextualizedDefinition<'a> { - pub definition: Definition<'a>, + pub definition: &'a Definition<'a>, pub visibility: Visibility, } From f14edd8661e2803254f89265548c7487f47a09f6 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:46:41 -0500 Subject: [PATCH 106/261] Bump 0.15.4 (#23595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Just to be sure, I ran the example from #23587 again on this branch: ```console ~/astral/ruff on  brent/0.15.4 [$] is 📦 v0.15.4 via 🐍 v3.14.2 via 🦀 v1.93.0 ❯ echo 'x = id' | uvx ruff@latest --isolated check - --preview --select ANN003,PLR1712 error: Ruff crashed. If you could open an issue at: https://github.com/astral-sh/ruff/issues/new?title=%5BPanic%5D ...quoting the executed command, along with the relevant file contents and `pyproject.toml` settings, we'd be very appreciative! thread 'main' (1681253) panicked at crates/ruff_python_semantic/src/definition.rs:285:26: index out of bounds: the len is 0 but the index is 0 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ~/astral/ruff on  brent/0.15.4 [$] is 📦 v0.15.4 via 🐍 v3.14.2 via 🦀 v1.93.0 ❯ echo 'x = id' | just run --isolated check - --preview --select ANN003,PLR1712 cargo run -p ruff -- --isolated check - --preview --select ANN003,PLR1712 Compiling ruff_python_semantic v0.0.0 (/home/brent/astral/ruff/crates/ruff_python_semantic) Compiling ruff v0.15.4 (/home/brent/astral/ruff/crates/ruff) Compiling ruff_linter v0.15.4 (/home/brent/astral/ruff/crates/ruff_linter) Compiling ruff_graph v0.1.0 (/home/brent/astral/ruff/crates/ruff_graph) Compiling ruff_workspace v0.0.0 (/home/brent/astral/ruff/crates/ruff_workspace) Compiling ruff_markdown v0.0.0 (/home/brent/astral/ruff/crates/ruff_markdown) Compiling ruff_server v0.2.2 (/home/brent/astral/ruff/crates/ruff_server) Finished `dev` profile [unoptimized + debuginfo] target(s) in 23.14s Running `target/debug/ruff --isolated check - --preview --select ANN003,PLR1712` warning: Detected debug build without --no-cache. All checks passed! ``` --- CHANGELOG.md | 23 +++++++++++++++++++++++ Cargo.lock | 6 +++--- README.md | 6 +++--- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/formatter.md | 2 +- docs/integrations.md | 8 ++++---- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 11 files changed, 40 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94d4b1759bbf1..e11124b647a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 0.15.4 + +Released on 2026-02-26. + +This is a follow-up release to 0.15.3 that resolves a panic when the new rule `PLR1712` was enabled with any rule that analyzes definitions, such as many of the `ANN` or `D` rules. + +### Bug fixes + +- Fix panic on access to definitions after analyzing definitions ([#23588](https://github.com/astral-sh/ruff/pull/23588)) +- \[`pyflakes`\] Suppress false positive in `F821` for names used before `del` in stub files ([#23550](https://github.com/astral-sh/ruff/pull/23550)) + +### Documentation + +- Clarify first-party import detection in Ruff ([#23591](https://github.com/astral-sh/ruff/pull/23591)) +- Fix incorrect `import-heading` example ([#23568](https://github.com/astral-sh/ruff/pull/23568)) + +### Contributors + +- [@stakeswky](https://github.com/stakeswky) +- [@ntBre](https://github.com/ntBre) +- [@thejcannon](https://github.com/thejcannon) +- [@GeObts](https://github.com/GeObts) + ## 0.15.3 Released on 2026-02-26. diff --git a/Cargo.lock b/Cargo.lock index 47f80fd269b8d..b2fb4a3ecdd35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3026,7 +3026,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.15.3" +version = "0.15.4" dependencies = [ "anyhow", "argfile", @@ -3289,7 +3289,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.3" +version = "0.15.4" dependencies = [ "aho-corasick", "anyhow", @@ -3663,7 +3663,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.15.3" +version = "0.15.4" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index a17bf9f0cd1a5..a46518c4b019e 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.15.3/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.15.3/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.15.4/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.15.4/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -186,7 +186,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.4 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 4d4fb9c633409..1cbad4154b715 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.15.3" +version = "0.15.4" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index c610dfa633426..44eebf243bf0a 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.15.3" +version = "0.15.4" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index be4ca1ddd0cca..9924202371aa8 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.15.3" +version = "0.15.4" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index 4d07a52cf58b8..8cfa827a56047 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -328,7 +328,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.3 + rev: v0.15.4 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/integrations.md b/docs/integrations.md index b2e0f45ae0319..0b554298d558f 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.15.3-alpine + name: ghcr.io/astral-sh/ruff:0.15.4-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.4 hooks: # Run the linter. - id: ruff-check @@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.4 hooks: # Run the linter. - id: ruff-check @@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.4 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 99b539fb8aad1..a1767480e9d62 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.4 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 68efc9e3ad49e..e32a04f3de6c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.15.3" +version = "0.15.4" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 6b69e82c34cfe..c4c7e3b729cbe 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.15.3" +version = "0.15.4" description = "" authors = ["Charles Marsh "] From 9982ad302a0eaeb648eb83ffd443d7252050feea Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Thu, 26 Feb 2026 13:24:50 -0800 Subject: [PATCH 107/261] Document extension mapping for markdown code formatting (#23574) Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- docs/formatter.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/formatter.md b/docs/formatter.md index 8cfa827a56047..ebf09308aa5ad 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -322,6 +322,25 @@ with [`extend-include`](settings.md#extend-include) in your project settings: extend-include = ["docs/*.md"] ``` +To format Markdown files with extensions other than `.md`, configure custom +[`extension`](settings.md#extension) mappings. Ruff will automatically include +these mapped extensions in file discovery: + +=== "pyproject.toml" + + ```toml + [tool.ruff] + # Treat `.mdx` and `.qmd` files as Markdown + extension = { mdx = "markdown", qmd = "markdown" } + ``` + +=== "ruff.toml" + + ```toml + # Treat `.mdx` and `.qmd` files as Markdown + extension = {mdx="markdown", qmd="markdown"} + ``` + If you run Ruff via [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit), Markdown support needs to be explicitly included by adding it to `types_or`: From 13b983c55a88f1fe5f96324591d0db4fb9a17099 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 26 Feb 2026 17:28:04 -0800 Subject: [PATCH 108/261] [ty] disallow negative narrowing on SubclassOf types (#23598) ## Summary We can't negatively narrow on an `isinstance` or `issubclass` check against a variable of type `type[X]`, because at runtime the value of the variable could be a subclass of `X`. ## Test Plan Added mdtest. --- .../resources/mdtest/narrow/isinstance.md | 14 +++ crates/ty_python_semantic/src/types/narrow.rs | 98 +++++++++++++------ 2 files changed, 80 insertions(+), 32 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 4e7efb7a10389..b3fcd35aef69f 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -337,6 +337,20 @@ def _(x: object, y: type[int]): reveal_type(x) # revealed: int ``` +Negative narrowing is not sound in this case, because `type[A]` includes subclasses of `A`: + +```py +class A: ... +class B: ... + +def f(x: A | B, y: type[A]): + if isinstance(x, y): + reveal_type(x) # revealed: A + return + + reveal_type(x) # revealed: A | B +``` + ## Adding a disjoint element to an existing intersection We used to incorrectly infer `Literal` booleans for some of these. diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 338eb2cb6e06e..c5d67c13254c9 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -145,7 +145,12 @@ impl ClassInfoConstraintFunction { /// /// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604 /// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type. - fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { + fn generate_constraint<'db>( + self, + db: &'db dyn Db, + classinfo: Type<'db>, + is_positive: bool, + ) -> Option> { let constraint_from_class_literal = |class: ClassLiteral<'db>| match self { ClassInfoConstraintFunction::IsInstance => { Type::instance(db, class.top_materialization(db)) @@ -156,27 +161,44 @@ impl ClassInfoConstraintFunction { }; match classinfo { - Type::TypeAlias(alias) => self.generate_constraint(db, alias.value_type(db)), + Type::TypeAlias(alias) => { + self.generate_constraint(db, alias.value_type(db), is_positive) + } Type::ClassLiteral(class_literal) => Some(constraint_from_class_literal(class_literal)), - Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - SubclassOfInner::Class(ClassType::NonGeneric(class_literal)) => { - Some(constraint_from_class_literal(class_literal)) + Type::SubclassOf(subclass_of_ty) => { + // We can't narrow negatively from a `SubclassOf` type. `if !isinstance(x, y)` + // where `y: type[A]` doesn't ensure that `x` is not an instance of `A`, because + // `y` could be some subclass of `A`. + if !is_positive { + return None; } - // It's not valid to use a generic alias as the second argument to `isinstance()` or `issubclass()`, - // e.g. `isinstance(x, list[int])` fails at runtime. - SubclassOfInner::Class(ClassType::Generic(_)) => None, - SubclassOfInner::Dynamic(dynamic) => Some(Type::Dynamic(dynamic)), - SubclassOfInner::TypeVar(bound_typevar) => match self { - ClassInfoConstraintFunction::IsSubclass => Some(classinfo), - ClassInfoConstraintFunction::IsInstance => Some(Type::TypeVar(bound_typevar)), - }, - }, + + match subclass_of_ty.subclass_of() { + SubclassOfInner::Class(ClassType::NonGeneric(class_literal)) => { + Some(constraint_from_class_literal(class_literal)) + } + // It's not valid to use a generic alias as the second argument to `isinstance()` or `issubclass()`, + // e.g. `isinstance(x, list[int])` fails at runtime. + SubclassOfInner::Class(ClassType::Generic(_)) => None, + SubclassOfInner::Dynamic(dynamic) => Some(Type::Dynamic(dynamic)), + SubclassOfInner::TypeVar(bound_typevar) => match self { + ClassInfoConstraintFunction::IsSubclass => Some(classinfo), + ClassInfoConstraintFunction::IsInstance => { + Some(Type::TypeVar(bound_typevar)) + } + }, + } + } Type::Dynamic(_) => Some(classinfo), Type::Intersection(intersection) => { if intersection.negative(db).is_empty() { let mut builder = IntersectionBuilder::new(db); for element in intersection.positive(db) { - builder = builder.add_positive(self.generate_constraint(db, *element)?); + builder = builder.add_positive(self.generate_constraint( + db, + *element, + is_positive, + )?); } Some(builder.build()) } else { @@ -184,16 +206,16 @@ impl ClassInfoConstraintFunction { None } } - Type::Union(union) => { - union.try_map(db, |element| self.generate_constraint(db, *element)) - } + Type::Union(union) => union.try_map(db, |element| { + self.generate_constraint(db, *element, is_positive) + }), Type::TypeVar(bound_typevar) => { match bound_typevar.typevar(db).bound_or_constraints(db)? { TypeVarBoundOrConstraints::UpperBound(bound) => { - self.generate_constraint(db, bound) + self.generate_constraint(db, bound, is_positive) } TypeVarBoundOrConstraints::Constraints(constraints) => { - self.generate_constraint(db, constraints.as_type(db)) + self.generate_constraint(db, constraints.as_type(db), is_positive) } } } @@ -207,7 +229,7 @@ impl ClassInfoConstraintFunction { db, tuple .iter_all_elements() - .map(|element| self.generate_constraint(db, element)), + .map(|element| self.generate_constraint(db, element, is_positive)), ) }), @@ -220,23 +242,31 @@ impl ClassInfoConstraintFunction { // which means that `isinstance(x, int | None)` works even though // `None` is not a class literal. if element.is_none(db) { - self.generate_constraint(db, KnownClass::NoneType.to_class_literal(db)) + self.generate_constraint( + db, + KnownClass::NoneType.to_class_literal(db), + is_positive, + ) } else { - self.generate_constraint(db, element) + self.generate_constraint(db, element, is_positive) } }), ) } Type::SpecialForm(form) => match form { - SpecialFormType::LegacyStdlibAlias(alias) => { - self.generate_constraint(db, alias.aliased_class().to_class_literal(db)) - } - SpecialFormType::Tuple => { - self.generate_constraint(db, KnownClass::Tuple.to_class_literal(db)) - } + SpecialFormType::LegacyStdlibAlias(alias) => self.generate_constraint( + db, + alias.aliased_class().to_class_literal(db), + is_positive, + ), + SpecialFormType::Tuple => self.generate_constraint( + db, + KnownClass::Tuple.to_class_literal(db), + is_positive, + ), SpecialFormType::Type => { - self.generate_constraint(db, KnownClass::Type.to_class_literal(db)) + self.generate_constraint(db, KnownClass::Type.to_class_literal(db), is_positive) } // We don't have a good meta-type for `Callable`s right now, @@ -1503,7 +1533,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { let class_info_ty = inference.expression_type(second_arg); function - .generate_constraint(self.db, class_info_ty) + .generate_constraint(self.db, class_info_ty, is_positive) .map(|constraint| { NarrowingConstraints::from_iter([( place, @@ -1635,7 +1665,11 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { let place = self.expect_place(&subject); let mapping_type = ClassInfoConstraintFunction::IsInstance - .generate_constraint(self.db, KnownClass::Mapping.to_class_literal(self.db))? + .generate_constraint( + self.db, + KnownClass::Mapping.to_class_literal(self.db), + is_positive, + )? .negate_if(self.db, !is_positive); Some(NarrowingConstraints::from_iter([( From 90fa4fd24f5255f9d3564607193a7cb0254d6ee5 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 27 Feb 2026 07:22:37 +0100 Subject: [PATCH 109/261] [ty] Add snapshot tests for advanced `invalid-assignment` scenarios (#23581) ## Summary Add some basic scenarios for `invalid-assignment` diagnostics that we plan to improve in the near future. The snapshots don't really need to be reviewed (I skimmed them). They only document the status quo. --- .../diagnostics/invalid_assignment_details.md | 250 ++++++++++++++++++ ... invalid_assignment_syntactic_variants.md} | 0 ...2\200\246_-_Basic_(7e8ff12bff1e8ba1).snap" | 34 +++ ...ncomp\342\200\246_(4771d5c9736f1df8).snap" | 39 +++ ...abili\342\200\246_(c38a5ba9bdfd90e8).snap" | 102 +++++++ ..._inco\342\200\246_(9d79916b62cea322).snap" | 44 +++ ...0\246_-_Protocols_(d6d4caa1b1180b74).snap" | 63 +++++ ...\200\246_-_Tuples_(fe1bc35fec6e57b4).snap" | 53 ++++ ...46_-_Type_aliases_(8ab0fe5706e7da9e).snap" | 45 ++++ ...\200\246_-_Unions_(4434e7e4a696d6d5).snap" | 72 +++++ ...\246_-_`Callable`_(d447753c67f673ad).snap" | 117 ++++++++ ...246_-_`TypedDict`_(c8d8ad73050ae4d7).snap" | 85 ++++++ ...otated_assignment_(b0568dbda1e94374).snap" | 4 +- ...ssion\342\200\246_(429392d5a8842ca6).snap" | 4 +- ..._Multiple_targets_(655e9238f07236b2).snap" | 4 +- ..._Named_expression_(f3e81bd84a3c9ca3).snap" | 4 +- ...ignme\342\200\246_(9ca7498412f218b3).snap" | 4 +- 17 files changed, 914 insertions(+), 10 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md rename crates/ty_python_semantic/resources/mdtest/diagnostics/{invalid_assignment.md => invalid_assignment_syntactic_variants.md} (100%) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Basic_(7e8ff12bff1e8ba1).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Deeply_nested_incomp\342\200\246_(4771d5c9736f1df8).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Function_assignabili\342\200\246_(c38a5ba9bdfd90e8).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_nested_inco\342\200\246_(9d79916b62cea322).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Protocols_(d6d4caa1b1180b74).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Tuples_(fe1bc35fec6e57b4).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Type_aliases_(8ab0fe5706e7da9e).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unions_(4434e7e4a696d6d5).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_`Callable`_(d447753c67f673ad).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_`TypedDict`_(c8d8ad73050ae4d7).snap" rename "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Annotated_assignment_(4b799ca1eeb857b9).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Annotated_assignment_(b0568dbda1e94374).snap" (79%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiline_expression\342\200\246_(f316976ffe72c6c7).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiline_expression\342\200\246_(429392d5a8842ca6).snap" (82%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_targets_(e20ddfd7a91affb0).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_targets_(655e9238f07236b2).snap" (88%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Named_expression_(35c120b3bd9929f8).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Named_expression_(f3e81bd84a3c9ca3).snap" (80%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unannotated_assignme\342\200\246_(67e4b9239d5681a).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unannotated_assignme\342\200\246_(9ca7498412f218b3).snap" (79%) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md new file mode 100644 index 0000000000000..cc5f2b3d04dff --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md @@ -0,0 +1,250 @@ +# Invalid assignment diagnostics + + + +```toml +[environment] +python-version = "3.12" +``` + +This file contains various scenarios of `invalid-assignment` (and related) diagnostics where we +(attempt to) do better than just report "type X is not assignable to type Y". + +## Basic + +Mainly for comparison: this is the most basic kind of `invalid-assignment` diagnostic: + +```py +def _(source: str): + target: bytes = source # error: [invalid-assignment] +``` + +## Unions + +Assigning a union to a non-union: + +```py +def _(source: str | None): + target: str = source # error: [invalid-assignment] +``` + +Assigning a non-union to a union: + +```py +def _(source: int): + target: str | None = source # error: [invalid-assignment] +``` + +Assigning a union to a union: + +```py +def _(source: str | None): + target: bytes | None = source # error: [invalid-assignment] +``` + +## Tuples + +Wrong element types: + +```py +def _(source: tuple[int, str, bool]): + target: tuple[int, bytes, bool] = source # error: [invalid-assignment] +``` + +Wrong number of elements: + +```py +def _(source: tuple[int, str]): + target: tuple[int, str, bool] = source # error: [invalid-assignment] +``` + +## `Callable` + +Assigning a function to a `Callable` + +```py +from typing import Any, Callable + +def source(x: int, y: str) -> None: + raise NotImplementedError + +target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] +``` + +Assigning a `Callable` to a `Callable` with wrong parameter type: + +```py +def _(source: Callable[[int, str], bool]): + target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] +``` + +Assigning a `Callable` to a `Callable` with wrong return type: + +```py +def _(source: Callable[[int, bytes], None]): + target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] +``` + +Assigning a `Callable` to a `Callable` with wrong number of parameters: + +```py +def _(source: Callable[[int, str], bool]): + target: Callable[[int], bool] = source # error: [invalid-assignment] +``` + +Assigning a class to a `Callable` + +```py +class Number: + def __init__(self, value: int): ... + +target: Callable[[str], Any] = Number # error: [invalid-assignment] +``` + +## Function assignability and overrides + +Liskov checks use function-to-function assignability. + +Wrong parameter type: + +```py +class Parent: + def method(self, x: str) -> bool: + raise NotImplementedError + +class Child1(Parent): + # error: [invalid-method-override] + def method(self, x: bytes) -> bool: + raise NotImplementedError +``` + +Wrong return type: + +```py +class Child2(Parent): + # error: [invalid-method-override] + def method(self, x: str) -> None: + raise NotImplementedError +``` + +Wrong non-positional-only parameter name: + +```py +class Child3(Parent): + # error: [invalid-method-override] + def method(self, y: str): + raise NotImplementedError +``` + +## `TypedDict` + +Incompatible field types: + +```py +from typing import Any, TypedDict + +class Person(TypedDict): + name: str + +class Other(TypedDict): + name: bytes + +def _(source: Person): + target: Other = source # error: [invalid-assignment] +``` + +Missing required fields: + +```py +class PersonWithAge(TypedDict): + name: str + age: int + +def _(source: Person): + target: PersonWithAge = source # error: [invalid-assignment] +``` + +Assigning a `TypedDict` to a `dict` + +```py +class Person(TypedDict): + name: str + +def _(source: Person): + target: dict[str, Any] = source # error: [invalid-assignment] +``` + +## Protocols + +Missing protocol members: + +```py +from typing import Protocol + +class SupportsCheck(Protocol): + def check(self, x: int, y: str) -> bool: ... + +class DoesNotHaveCheck: ... + +def _(source: DoesNotHaveCheck): + target: SupportsCheck = source # error: [invalid-assignment] +``` + +Incompatible types for protocol members: + +```py +class CheckWithWrongSignature: + def check(self, x: int, y: bytes) -> bool: + return False + +def _(source: CheckWithWrongSignature): + target: SupportsCheck = source # error: [invalid-assignment] +``` + +## Type aliases + +Type aliases should be expanded in diagnostics to understand the underlying incompatibilities: + +```py +from typing import Protocol + +class SupportsName(Protocol): + def name(self) -> str: ... + +class HasName: + def name(self) -> bytes: + return b"" + +type StringOrName = str | SupportsName + +def _(source: HasName): + target: SupportsName = source # error: [invalid-assignment] +``` + +## Deeply nested incompatibilities + +```py +from typing import Callable + +def source(x: tuple[int, str]) -> bool: + return False + +target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment] +``` + +## Multiple nested incompatibilities + +```py +from typing import Protocol + +class SupportsCheck(Protocol): + def check1(self, x: str): ... + def check2(self, x: int) -> bool: ... + +class Incompatible: + def check1(self, x: bytes): ... + def check2(self, x: int) -> None: ... + +def _(source: Incompatible): + target: SupportsCheck = source # error: [invalid-assignment] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md similarity index 100% rename from crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Basic_(7e8ff12bff1e8ba1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Basic_(7e8ff12bff1e8ba1).snap" new file mode 100644 index 0000000000000..abae1cebb867c --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Basic_(7e8ff12bff1e8ba1).snap" @@ -0,0 +1,34 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Basic +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def _(source: str): +2 | target: bytes = source # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `str` is not assignable to `bytes` + --> src/mdtest_snippet.py:2:13 + | +1 | def _(source: str): +2 | target: bytes = source # error: [invalid-assignment] + | ----- ^^^^^^ Incompatible value of type `str` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Deeply_nested_incomp\342\200\246_(4771d5c9736f1df8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Deeply_nested_incomp\342\200\246_(4771d5c9736f1df8).snap" new file mode 100644 index 0000000000000..490bacee0a54d --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Deeply_nested_incomp\342\200\246_(4771d5c9736f1df8).snap" @@ -0,0 +1,39 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Deeply nested incompatibilities +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import Callable +2 | +3 | def source(x: tuple[int, str]) -> bool: +4 | return False +5 | +6 | target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `def source(x: tuple[int, str]) -> bool` is not assignable to `(tuple[int, bytes], /) -> bool` + --> src/mdtest_snippet.py:6:9 + | +4 | return False +5 | +6 | target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment] + | ----------------------------------- ^^^^^^ Incompatible value of type `def source(x: tuple[int, str]) -> bool` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Function_assignabili\342\200\246_(c38a5ba9bdfd90e8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Function_assignabili\342\200\246_(c38a5ba9bdfd90e8).snap" new file mode 100644 index 0000000000000..ba6141ea86724 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Function_assignabili\342\200\246_(c38a5ba9bdfd90e8).snap" @@ -0,0 +1,102 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Function assignability and overrides +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Parent: + 2 | def method(self, x: str) -> bool: + 3 | raise NotImplementedError + 4 | + 5 | class Child1(Parent): + 6 | # error: [invalid-method-override] + 7 | def method(self, x: bytes) -> bool: + 8 | raise NotImplementedError + 9 | class Child2(Parent): +10 | # error: [invalid-method-override] +11 | def method(self, x: str) -> None: +12 | raise NotImplementedError +13 | class Child3(Parent): +14 | # error: [invalid-method-override] +15 | def method(self, y: str): +16 | raise NotImplementedError +``` + +# Diagnostics + +``` +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.py:7:9 + | +5 | class Child1(Parent): +6 | # error: [invalid-method-override] +7 | def method(self, x: bytes) -> bool: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +8 | raise NotImplementedError +9 | class Child2(Parent): + | + ::: src/mdtest_snippet.py:2:9 + | +1 | class Parent: +2 | def method(self, x: str) -> bool: + | ---------------------------- `Parent.method` defined here +3 | raise NotImplementedError + | +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + +``` +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.py:11:9 + | + 9 | class Child2(Parent): +10 | # error: [invalid-method-override] +11 | def method(self, x: str) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +12 | raise NotImplementedError +13 | class Child3(Parent): + | + ::: src/mdtest_snippet.py:2:9 + | + 1 | class Parent: + 2 | def method(self, x: str) -> bool: + | ---------------------------- `Parent.method` defined here + 3 | raise NotImplementedError + | +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + +``` +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.py:15:9 + | +13 | class Child3(Parent): +14 | # error: [invalid-method-override] +15 | def method(self, y: str): + | ^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +16 | raise NotImplementedError + | + ::: src/mdtest_snippet.py:2:9 + | + 1 | class Parent: + 2 | def method(self, x: str) -> bool: + | ---------------------------- `Parent.method` defined here + 3 | raise NotImplementedError + | +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_nested_inco\342\200\246_(9d79916b62cea322).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_nested_inco\342\200\246_(9d79916b62cea322).snap" new file mode 100644 index 0000000000000..9786a73cb64f0 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_nested_inco\342\200\246_(9d79916b62cea322).snap" @@ -0,0 +1,44 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Multiple nested incompatibilities +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Protocol + 2 | + 3 | class SupportsCheck(Protocol): + 4 | def check1(self, x: str): ... + 5 | def check2(self, x: int) -> bool: ... + 6 | + 7 | class Incompatible: + 8 | def check1(self, x: bytes): ... + 9 | def check2(self, x: int) -> None: ... +10 | +11 | def _(source: Incompatible): +12 | target: SupportsCheck = source # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `Incompatible` is not assignable to `SupportsCheck` + --> src/mdtest_snippet.py:12:13 + | +11 | def _(source: Incompatible): +12 | target: SupportsCheck = source # error: [invalid-assignment] + | ------------- ^^^^^^ Incompatible value of type `Incompatible` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Protocols_(d6d4caa1b1180b74).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Protocols_(d6d4caa1b1180b74).snap" new file mode 100644 index 0000000000000..5456d31509621 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Protocols_(d6d4caa1b1180b74).snap" @@ -0,0 +1,63 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Protocols +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Protocol + 2 | + 3 | class SupportsCheck(Protocol): + 4 | def check(self, x: int, y: str) -> bool: ... + 5 | + 6 | class DoesNotHaveCheck: ... + 7 | + 8 | def _(source: DoesNotHaveCheck): + 9 | target: SupportsCheck = source # error: [invalid-assignment] +10 | class CheckWithWrongSignature: +11 | def check(self, x: int, y: bytes) -> bool: +12 | return False +13 | +14 | def _(source: CheckWithWrongSignature): +15 | target: SupportsCheck = source # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `DoesNotHaveCheck` is not assignable to `SupportsCheck` + --> src/mdtest_snippet.py:9:13 + | + 8 | def _(source: DoesNotHaveCheck): + 9 | target: SupportsCheck = source # error: [invalid-assignment] + | ------------- ^^^^^^ Incompatible value of type `DoesNotHaveCheck` + | | + | Declared type +10 | class CheckWithWrongSignature: +11 | def check(self, x: int, y: bytes) -> bool: + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `CheckWithWrongSignature` is not assignable to `SupportsCheck` + --> src/mdtest_snippet.py:15:13 + | +14 | def _(source: CheckWithWrongSignature): +15 | target: SupportsCheck = source # error: [invalid-assignment] + | ------------- ^^^^^^ Incompatible value of type `CheckWithWrongSignature` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Tuples_(fe1bc35fec6e57b4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Tuples_(fe1bc35fec6e57b4).snap" new file mode 100644 index 0000000000000..4dd01bdd7a769 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Tuples_(fe1bc35fec6e57b4).snap" @@ -0,0 +1,53 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Tuples +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def _(source: tuple[int, str, bool]): +2 | target: tuple[int, bytes, bool] = source # error: [invalid-assignment] +3 | def _(source: tuple[int, str]): +4 | target: tuple[int, str, bool] = source # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `tuple[int, str, bool]` is not assignable to `tuple[int, bytes, bool]` + --> src/mdtest_snippet.py:2:13 + | +1 | def _(source: tuple[int, str, bool]): +2 | target: tuple[int, bytes, bool] = source # error: [invalid-assignment] + | ----------------------- ^^^^^^ Incompatible value of type `tuple[int, str, bool]` + | | + | Declared type +3 | def _(source: tuple[int, str]): +4 | target: tuple[int, str, bool] = source # error: [invalid-assignment] + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `tuple[int, str]` is not assignable to `tuple[int, str, bool]` + --> src/mdtest_snippet.py:4:13 + | +2 | target: tuple[int, bytes, bool] = source # error: [invalid-assignment] +3 | def _(source: tuple[int, str]): +4 | target: tuple[int, str, bool] = source # error: [invalid-assignment] + | --------------------- ^^^^^^ Incompatible value of type `tuple[int, str]` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Type_aliases_(8ab0fe5706e7da9e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Type_aliases_(8ab0fe5706e7da9e).snap" new file mode 100644 index 0000000000000..d17a05058f8a1 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Type_aliases_(8ab0fe5706e7da9e).snap" @@ -0,0 +1,45 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Type aliases +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Protocol + 2 | + 3 | class SupportsName(Protocol): + 4 | def name(self) -> str: ... + 5 | + 6 | class HasName: + 7 | def name(self) -> bytes: + 8 | return b"" + 9 | +10 | type StringOrName = str | SupportsName +11 | +12 | def _(source: HasName): +13 | target: SupportsName = source # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `HasName` is not assignable to `SupportsName` + --> src/mdtest_snippet.py:13:13 + | +12 | def _(source: HasName): +13 | target: SupportsName = source # error: [invalid-assignment] + | ------------ ^^^^^^ Incompatible value of type `HasName` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unions_(4434e7e4a696d6d5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unions_(4434e7e4a696d6d5).snap" new file mode 100644 index 0000000000000..6d8821152bc27 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unions_(4434e7e4a696d6d5).snap" @@ -0,0 +1,72 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Unions +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def _(source: str | None): +2 | target: str = source # error: [invalid-assignment] +3 | def _(source: int): +4 | target: str | None = source # error: [invalid-assignment] +5 | def _(source: str | None): +6 | target: bytes | None = source # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `str | None` is not assignable to `str` + --> src/mdtest_snippet.py:2:13 + | +1 | def _(source: str | None): +2 | target: str = source # error: [invalid-assignment] + | --- ^^^^^^ Incompatible value of type `str | None` + | | + | Declared type +3 | def _(source: int): +4 | target: str | None = source # error: [invalid-assignment] + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `int` is not assignable to `str | None` + --> src/mdtest_snippet.py:4:13 + | +2 | target: str = source # error: [invalid-assignment] +3 | def _(source: int): +4 | target: str | None = source # error: [invalid-assignment] + | ---------- ^^^^^^ Incompatible value of type `int` + | | + | Declared type +5 | def _(source: str | None): +6 | target: bytes | None = source # error: [invalid-assignment] + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `str | None` is not assignable to `bytes | None` + --> src/mdtest_snippet.py:6:13 + | +4 | target: str | None = source # error: [invalid-assignment] +5 | def _(source: str | None): +6 | target: bytes | None = source # error: [invalid-assignment] + | ------------ ^^^^^^ Incompatible value of type `str | None` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_`Callable`_(d447753c67f673ad).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_`Callable`_(d447753c67f673ad).snap" new file mode 100644 index 0000000000000..01f9a8477fc6b --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_`Callable`_(d447753c67f673ad).snap" @@ -0,0 +1,117 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - `Callable` +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Any, Callable + 2 | + 3 | def source(x: int, y: str) -> None: + 4 | raise NotImplementedError + 5 | + 6 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + 7 | def _(source: Callable[[int, str], bool]): + 8 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + 9 | def _(source: Callable[[int, bytes], None]): +10 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] +11 | def _(source: Callable[[int, str], bool]): +12 | target: Callable[[int], bool] = source # error: [invalid-assignment] +13 | class Number: +14 | def __init__(self, value: int): ... +15 | +16 | target: Callable[[str], Any] = Number # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `def source(x: int, y: str) -> None` is not assignable to `(int, bytes, /) -> bool` + --> src/mdtest_snippet.py:6:9 + | +4 | raise NotImplementedError +5 | +6 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + | ---------------------------- ^^^^^^ Incompatible value of type `def source(x: int, y: str) -> None` + | | + | Declared type +7 | def _(source: Callable[[int, str], bool]): +8 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `(int, str, /) -> bool` is not assignable to `(int, bytes, /) -> bool` + --> src/mdtest_snippet.py:8:13 + | + 6 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + 7 | def _(source: Callable[[int, str], bool]): + 8 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + | ---------------------------- ^^^^^^ Incompatible value of type `(int, str, /) -> bool` + | | + | Declared type + 9 | def _(source: Callable[[int, bytes], None]): +10 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `(int, bytes, /) -> None` is not assignable to `(int, bytes, /) -> bool` + --> src/mdtest_snippet.py:10:13 + | + 8 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + 9 | def _(source: Callable[[int, bytes], None]): +10 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] + | ---------------------------- ^^^^^^ Incompatible value of type `(int, bytes, /) -> None` + | | + | Declared type +11 | def _(source: Callable[[int, str], bool]): +12 | target: Callable[[int], bool] = source # error: [invalid-assignment] + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `(int, str, /) -> bool` is not assignable to `(int, /) -> bool` + --> src/mdtest_snippet.py:12:13 + | +10 | target: Callable[[int, bytes], bool] = source # error: [invalid-assignment] +11 | def _(source: Callable[[int, str], bool]): +12 | target: Callable[[int], bool] = source # error: [invalid-assignment] + | --------------------- ^^^^^^ Incompatible value of type `(int, str, /) -> bool` + | | + | Declared type +13 | class Number: +14 | def __init__(self, value: int): ... + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `` is not assignable to `(str, /) -> Any` + --> src/mdtest_snippet.py:16:9 + | +14 | def __init__(self, value: int): ... +15 | +16 | target: Callable[[str], Any] = Number # error: [invalid-assignment] + | -------------------- ^^^^^^ Incompatible value of type `` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_`TypedDict`_(c8d8ad73050ae4d7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_`TypedDict`_(c8d8ad73050ae4d7).snap" new file mode 100644 index 0000000000000..5a3ec8d1fc1ce --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_`TypedDict`_(c8d8ad73050ae4d7).snap" @@ -0,0 +1,85 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - `TypedDict` +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Any, TypedDict + 2 | + 3 | class Person(TypedDict): + 4 | name: str + 5 | + 6 | class Other(TypedDict): + 7 | name: bytes + 8 | + 9 | def _(source: Person): +10 | target: Other = source # error: [invalid-assignment] +11 | class PersonWithAge(TypedDict): +12 | name: str +13 | age: int +14 | +15 | def _(source: Person): +16 | target: PersonWithAge = source # error: [invalid-assignment] +17 | class Person(TypedDict): +18 | name: str +19 | +20 | def _(source: Person): +21 | target: dict[str, Any] = source # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `Person` is not assignable to `Other` + --> src/mdtest_snippet.py:10:13 + | + 9 | def _(source: Person): +10 | target: Other = source # error: [invalid-assignment] + | ----- ^^^^^^ Incompatible value of type `Person` + | | + | Declared type +11 | class PersonWithAge(TypedDict): +12 | name: str + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `Person` is not assignable to `PersonWithAge` + --> src/mdtest_snippet.py:16:13 + | +15 | def _(source: Person): +16 | target: PersonWithAge = source # error: [invalid-assignment] + | ------------- ^^^^^^ Incompatible value of type `Person` + | | + | Declared type +17 | class Person(TypedDict): +18 | name: str + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `Person` is not assignable to `dict[str, Any]` + --> src/mdtest_snippet.py:21:13 + | +20 | def _(source: Person): +21 | target: dict[str, Any] = source # error: [invalid-assignment] + | -------------- ^^^^^^ Incompatible value of type `Person` + | | + | Declared type + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Annotated_assignment_(4b799ca1eeb857b9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Annotated_assignment_(b0568dbda1e94374).snap" similarity index 79% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Annotated_assignment_(4b799ca1eeb857b9).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Annotated_assignment_(b0568dbda1e94374).snap" index 76a1dade8c94a..c176128c34df4 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Annotated_assignment_(4b799ca1eeb857b9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Annotated_assignment_(b0568dbda1e94374).snap" @@ -4,8 +4,8 @@ expression: snapshot --- --- -mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Annotated assignment -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md +mdtest name: invalid_assignment_syntactic_variants.md - Invalid assignment diagnostics - Annotated assignment +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md --- # Python source files diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiline_expression\342\200\246_(f316976ffe72c6c7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiline_expression\342\200\246_(429392d5a8842ca6).snap" similarity index 82% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiline_expression\342\200\246_(f316976ffe72c6c7).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiline_expression\342\200\246_(429392d5a8842ca6).snap" index a1e4da5e36d83..62c41660e9425 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiline_expression\342\200\246_(f316976ffe72c6c7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiline_expression\342\200\246_(429392d5a8842ca6).snap" @@ -4,8 +4,8 @@ expression: snapshot --- --- -mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Multiline expressions -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md +mdtest name: invalid_assignment_syntactic_variants.md - Invalid assignment diagnostics - Multiline expressions +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md --- # Python source files diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_targets_(e20ddfd7a91affb0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_targets_(655e9238f07236b2).snap" similarity index 88% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_targets_(e20ddfd7a91affb0).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_targets_(655e9238f07236b2).snap" index ebf3b8179962d..55bbf76bece8e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_targets_(e20ddfd7a91affb0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Multiple_targets_(655e9238f07236b2).snap" @@ -4,8 +4,8 @@ expression: snapshot --- --- -mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Multiple targets -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md +mdtest name: invalid_assignment_syntactic_variants.md - Invalid assignment diagnostics - Multiple targets +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md --- # Python source files diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Named_expression_(35c120b3bd9929f8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Named_expression_(f3e81bd84a3c9ca3).snap" similarity index 80% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Named_expression_(35c120b3bd9929f8).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Named_expression_(f3e81bd84a3c9ca3).snap" index d285720f31c4a..74f76cd2aef5d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Named_expression_(35c120b3bd9929f8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Named_expression_(f3e81bd84a3c9ca3).snap" @@ -4,8 +4,8 @@ expression: snapshot --- --- -mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Named expression -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md +mdtest name: invalid_assignment_syntactic_variants.md - Invalid assignment diagnostics - Named expression +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md --- # Python source files diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unannotated_assignme\342\200\246_(67e4b9239d5681a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unannotated_assignme\342\200\246_(9ca7498412f218b3).snap" similarity index 79% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unannotated_assignme\342\200\246_(67e4b9239d5681a).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unannotated_assignme\342\200\246_(9ca7498412f218b3).snap" index 3099de022c7eb..ab58fda9af5d8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unannotated_assignme\342\200\246_(67e4b9239d5681a).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_s\342\200\246_-_Invalid_assignment_d\342\200\246_-_Unannotated_assignme\342\200\246_(9ca7498412f218b3).snap" @@ -4,8 +4,8 @@ expression: snapshot --- --- -mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Unannotated assignment -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md +mdtest name: invalid_assignment_syntactic_variants.md - Invalid assignment diagnostics - Unannotated assignment +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md --- # Python source files From d0a231cc920c6adc950d10b896cd5b0d4d2483f1 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 27 Feb 2026 11:42:57 +0100 Subject: [PATCH 110/261] [ty] Dataclass transform: neither frozen nor non-frozen (#23366) ## Summary Implement the semantics described [here in the typing spec](https://typing.python.org/en/latest/spec/dataclasses.html#dataclass-semantics): > Frozen dataclasses cannot inherit from non-frozen dataclasses. A class that has been decorated with `dataclass_transform` **is considered neither frozen nor non-frozen**, thus allowing frozen classes to inherit from it. Similarly, a class that directly specifies a metaclass that is decorated with `dataclass_transform` is considered neither frozen nor non-frozen. ## Test Plan New Markdown tests for all flavors of `dataclass_transform`ers. --- crates/ty/docs/rules.md | 196 +++++++++--------- .../mdtest/dataclasses/dataclass_transform.md | 154 +++++++++++++- crates/ty_python_semantic/src/types.rs | 16 +- crates/ty_python_semantic/src/types/class.rs | 58 +++++- .../src/types/diagnostic.rs | 8 +- .../src/types/infer/builder.rs | 28 ++- 6 files changed, 320 insertions(+), 140 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 814eeae5068d0..178fb687ba991 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -640,7 +640,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -766,7 +766,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -796,7 +796,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -822,7 +822,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -856,7 +856,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -945,7 +945,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -972,7 +972,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1000,7 +1000,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1034,7 +1034,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1070,7 +1070,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1094,7 +1094,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1121,7 +1121,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1158,7 +1158,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1190,7 +1190,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1219,7 +1219,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1263,7 +1263,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1305,7 +1305,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1349,7 +1349,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1387,7 +1387,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1466,7 +1466,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1505,7 +1505,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1566,7 +1566,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1601,7 +1601,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1629,7 +1629,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1663,7 +1663,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1770,7 +1770,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1824,7 +1824,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1854,7 +1854,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1904,7 +1904,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1930,7 +1930,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1961,7 +1961,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1995,7 +1995,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2044,7 +2044,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2073,7 +2073,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2169,7 +2169,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2215,7 +2215,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2242,7 +2242,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2289,7 +2289,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2319,7 +2319,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2349,7 +2349,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2383,7 +2383,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2417,7 +2417,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2448,7 +2448,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2495,7 +2495,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2527,7 +2527,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2562,7 +2562,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2593,7 +2593,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2648,7 +2648,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2691,7 +2691,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2716,7 +2716,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2749,7 +2749,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2778,7 +2778,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2804,7 +2804,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2828,7 +2828,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2861,7 +2861,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2894,7 +2894,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2921,7 +2921,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2948,7 +2948,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2976,7 +2976,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3008,7 +3008,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3045,7 +3045,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3109,7 +3109,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3136,7 +3136,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3168,7 +3168,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3198,7 +3198,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3227,7 +3227,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3261,7 +3261,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3288,7 +3288,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3316,7 +3316,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3362,7 +3362,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3386,7 +3386,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3413,7 +3413,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3441,7 +3441,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3499,7 +3499,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3524,7 +3524,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3549,7 +3549,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3588,7 +3588,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3625,7 +3625,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3666,7 +3666,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3767,7 +3767,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3830,7 +3830,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md index 07810c75f1db5..079f55b43dcb2 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -421,9 +421,7 @@ class Mutable(DefaultFrozenModel, frozen=False, order=True): name: str m = Mutable(name="test") -# TODO: This should not be an error. In order to support this, we need to implement the precise `frozen` semantics of -# `dataclass_transform` described here: https://typing.python.org/en/latest/spec/dataclasses.html#dataclass-semantics -m.name = "new" # error: [invalid-assignment] +m.name = "new" # No error reveal_type(Mutable(name="A") < Mutable(name="B")) # revealed: bool ``` @@ -459,6 +457,154 @@ m.name = "new" # No error reveal_type(Mutable(name="A") < Mutable(name="B")) # revealed: bool ``` +### Frozen inheritance + +Just like for regular `@dataclass`es, mixing frozen and non-frozen `@dataclass_transform` classes in +an inheritance chain is not allowed. However, the root class of a `@dataclass_transform` hierarchy +(the class decorated with `@dataclass_transform()` or the class that directly specifies the +`@dataclass_transform` metaclass) is "neither frozen nor non-frozen", so both frozen and non-frozen +subclasses can inherit from it. + +#### Using function-based transformers + +For function-based transformers, all classes are either frozen or non-frozen. There is no special +root class. + +```py +from typing import dataclass_transform + +@dataclass_transform(frozen_default=True) +def frozen_model(*, frozen: bool = True): ... + +@frozen_model() +class FrozenParent: + x: int + +@frozen_model() +class FrozenChild(FrozenParent): + y: int + +@frozen_model(frozen=False) +# error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `NonFrozenChild` cannot inherit from frozen dataclass `FrozenParent`" +class NonFrozenChild(FrozenParent): + y: int + +@frozen_model(frozen=False) +class NonFrozenParent: + x: int + +@frozen_model() +# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenFromNonFrozen` cannot inherit from non-frozen dataclass `NonFrozenParent`" +class FrozenFromNonFrozen(NonFrozenParent): + y: int +``` + +#### Using metaclass-based transformers + +For metaclass-based transformers, the class that is decorated with `@dataclass_transform` is the +root class that is "neither frozen nor non-frozen" (`DefaultFrozenMeta` in the example below). So +children of that class can be either frozen or non-frozen: + +```py +from typing import dataclass_transform + +@dataclass_transform(frozen_default=True) +class FrozenMeta(type): + def __new__( + cls, + name, + bases, + namespace, + *, + frozen: bool = True, + ): ... + +class DefaultFrozenModel(metaclass=FrozenMeta): ... +``` + +Both frozen and non-frozen classes can inherit from the root class: + +```py +class FrozenParent(DefaultFrozenModel): + x: int + +class NonFrozenParent(DefaultFrozenModel, frozen=False): + x: int +``` + +Inheriting from these classes is fine as long as we keep the frozen/non-frozen status consistent: + +```py +class FrozenChild(FrozenParent): + y: int + +class NonFrozenChild(NonFrozenParent, frozen=False): + y: int +``` + +But mixing frozen and non-frozen is not allowed at this level: + +```py +# error: [invalid-frozen-dataclass-subclass] +class InvalidFrozenChild(NonFrozenParent, frozen=True): + y: int + +# error: [invalid-frozen-dataclass-subclass] +class InvalidNonFrozenChild(FrozenParent, frozen=False): + y: int +``` + +#### Using base-class-based transformers + +Similarly, for base-class-based transformers, the class that is decorated with +`@dataclass_transform` is the root class that is "neither frozen nor non-frozen" +(`DefaultFrozenModel` in the example below). So children of that class can be either frozen or +non-frozen: + +```py +from typing import dataclass_transform + +@dataclass_transform(frozen_default=True) +class DefaultFrozenModel: + def __init_subclass__( + cls, + *, + frozen: bool = True, + ): ... +``` + +Both frozen and non-frozen classes can inherit from that root model: + +```py +class FrozenParent(DefaultFrozenModel): + x: int + +class NonFrozenParent(DefaultFrozenModel, frozen=False): + x: int +``` + +Inheriting from these classes is fine as long as we keep the frozen/non-frozen status consistent: + +```py +class FrozenChild(FrozenParent): + y: int + +class NonFrozenChild(NonFrozenParent, frozen=False): + y: int +``` + +But mixing frozen and non-frozen is not allowed at this level: + +```py +# error: [invalid-frozen-dataclass-subclass] +class InvalidFrozenChild(NonFrozenParent, frozen=True): + y: int + +# error: [invalid-frozen-dataclass-subclass] +class InvalidNonFrozenChild(FrozenParent, frozen=False): + y: int +``` + ### Override diagnostics on dataclass-like classes #### Frozen override diagnostics @@ -1246,7 +1392,7 @@ sure that we recognize all fields in a hierarchy like this: from dataclasses import dataclass from typing import dataclass_transform -@dataclass_transform() +@dataclass_transform(frozen_default=True) class ModelMeta(type): pass diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b5132cec6306d..97fb4d22b11ff 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -542,12 +542,6 @@ bitflags! { } } -impl DataclassFlags { - pub(crate) const fn is_frozen(self) -> bool { - self.contains(Self::FROZEN) - } -} - pub(crate) const DATACLASS_FLAGS: &[(&str, DataclassFlags)] = &[ ("init", DataclassFlags::INIT), ("repr", DataclassFlags::REPR), @@ -11628,6 +11622,16 @@ pub(super) struct MetaclassCandidate<'db> { explicit_metaclass_of: StaticClassLiteral<'db>, } +/// Information about a `@dataclass_transform`-decorated metaclass. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(super) struct MetaclassTransformInfo<'db> { + pub(super) params: DataclassTransformerParams<'db>, + + /// Whether the metaclass providing these parameters was declared on the class itself + /// (via an explicit `metaclass=` keyword) rather than inherited from a base class. + pub(super) from_explicit_metaclass: bool, +} + #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct UnionType<'db> { /// The union type includes values in any of these types. diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index ca6a95a1dec99..f6ee46ee5a45d 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -59,8 +59,8 @@ use crate::{ semantic_index, use_def_map, }, types::{ - CallArguments, CallError, CallErrorKind, MetaclassCandidate, TypeDefinition, UnionType, - definition_expression_type, + CallArguments, CallError, CallErrorKind, MetaclassCandidate, MetaclassTransformInfo, + TypeDefinition, UnionType, definition_expression_type, }, }; use indexmap::IndexSet; @@ -119,7 +119,7 @@ fn try_metaclass_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, _self_: StaticClassLiteral<'db>, -) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { +) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { Err(MetaclassError { kind: MetaclassErrorKind::Cycle, }) @@ -180,8 +180,8 @@ impl<'db> CodeGeneratorKind<'db> { ) -> Option> { if class.dataclass_params(db).is_some() { Some(CodeGeneratorKind::DataclassLike(None)) - } else if let Ok((_, Some(transformer_params))) = class.try_metaclass(db) { - Some(CodeGeneratorKind::DataclassLike(Some(transformer_params))) + } else if let Ok((_, Some(info))) = class.try_metaclass(db) { + Some(CodeGeneratorKind::DataclassLike(Some(info.params))) } else if let Some(transformer_params) = class.iter_mro(db, specialization).skip(1).find_map(|base| { base.into_class().and_then(|class| { @@ -2734,6 +2734,38 @@ impl<'db> StaticClassLiteral<'db> { (dataclass_params, transformer_params) } + /// Returns the effective frozen status of this class if it's a dataclass-like class. + /// + /// Returns `Some(true)` for a frozen dataclass-like class, `Some(false)` for a non-frozen one, + /// and `None` if the class is not a dataclass-like class, or if the dataclass is neither frozen + /// nor non-frozen. + pub(crate) fn is_frozen_dataclass(self, db: &'db dyn Db) -> Option { + // Check if this is a base-class-based transformer that has dataclass_transformer_params directly + // attached to it (because it is itself decorated with `@dataclass_transform`), or if this class + // has an explicit metaclass that is decorated with `@dataclass_transform`. + // + // In both cases, this signifies that this class is neither frozen nor non-frozen. + // + // See for details. + if self.dataclass_transformer_params(db).is_some() + || self + .try_metaclass(db) + .is_ok_and(|(_, info)| info.is_some_and(|i| i.from_explicit_metaclass)) + { + return None; + } + + if let field_policy @ CodeGeneratorKind::DataclassLike(_) = + CodeGeneratorKind::from_class(db, self.into(), None)? + { + // Otherwise, if this class is a dataclass-like class, determine its frozen status based on + // dataclass params and dataclass transformer params. + Some(self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN)) + } else { + None + } + } + /// Checks if the given dataclass parameter flag is set for this class. /// This checks both the `dataclass_params` and `transformer_params`. fn has_dataclass_param( @@ -2783,7 +2815,7 @@ impl<'db> StaticClassLiteral<'db> { pub(super) fn try_metaclass( self, db: &'db dyn Db, - ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { + ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { tracing::trace!("StaticClassLiteral::try_metaclass: {}", self.name(db)); // Identify the class's own metaclass (or take the first base class's metaclass). @@ -2887,11 +2919,15 @@ impl<'db> StaticClassLiteral<'db> { }); } - let dataclass_transformer_params = candidate + let transform_info = candidate .metaclass .static_class_literal(db) - .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db)); - Ok((candidate.metaclass.into(), dataclass_transformer_params)) + .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db)) + .map(|params| MetaclassTransformInfo { + params, + from_explicit_metaclass: candidate.explicit_metaclass_of == self, + }); + Ok((candidate.metaclass.into(), transform_info)) } /// Returns the class member of this class named `name`. @@ -3461,7 +3497,7 @@ impl<'db> StaticClassLiteral<'db> { signature_from_fields(vec![self_parameter], instance_ty) } (CodeGeneratorKind::DataclassLike(_), "__setattr__") => { - if self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN) { + if self.is_frozen_dataclass(db) == Some(true) { let signature = Signature::new( Parameters::new( db, @@ -3959,7 +3995,7 @@ impl<'db> StaticClassLiteral<'db> { match name.as_str() { "__setattr__" | "__delattr__" => { if let CodeGeneratorKind::DataclassLike(_) = field_policy - && self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN) + && self.is_frozen_dataclass(db) == Some(true) { if let Some(builder) = context.report_lint( &INVALID_DATACLASS_OVERRIDE, diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index f708fd62e7ff9..6fcfec2fcc7d4 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -31,9 +31,7 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, protocol_class::ProtocolClass, }; -use crate::types::{ - DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance, UnionType, -}; +use crate::types::{KnownInstanceType, MemberLookupPolicy, TypeVarInstance, UnionType}; use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint}; use itertools::Itertools; use ruff_db::{ @@ -5566,7 +5564,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( class_node: &ast::StmtClassDef, base_class: StaticClassLiteral<'db>, base_class_node: &ast::Expr, - base_class_params: DataclassFlags, + base_is_frozen: bool, ) { let db = context.db(); @@ -5576,7 +5574,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( return; }; - let mut diagnostic = if base_class_params.is_frozen() { + let mut diagnostic = if base_is_frozen { let mut diagnostic = builder.into_diagnostic("Non-frozen dataclass cannot inherit from frozen dataclass"); diagnostic.set_concise_message(format_args!( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index e4917269554d4..cc66d51609d0e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1061,24 +1061,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if let Some((base_class_literal, _)) = base_class.static_class_literal(self.db()) - && let (Some(base_params), Some(class_params)) = ( - base_class_literal.dataclass_params(self.db()), - class.dataclass_params(self.db()), + && let (Some(base_is_frozen), Some(class_is_frozen)) = ( + base_class_literal.is_frozen_dataclass(self.db()), + class.is_frozen_dataclass(self.db()), ) + && base_is_frozen != class_is_frozen { - let base_params = base_params.flags(self.db()); - let class_is_frozen = class_params.flags(self.db()).is_frozen(); - - if base_params.is_frozen() != class_is_frozen { - report_bad_frozen_dataclass_inheritance( - &self.context, - class, - class_node, - base_class_literal, - &class_node.bases()[i], - base_params, - ); - } + report_bad_frozen_dataclass_inheritance( + &self.context, + class, + class_node, + base_class_literal, + &class_node.bases()[i], + base_is_frozen, + ); } } From bf602897c144d86b3c9950632ef5721f230bffcd Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 27 Feb 2026 07:40:05 -0500 Subject: [PATCH 111/261] [ty] Refactor to support building constraint sets differently (#23600) This is a pure refactoring PR that exists solely to set up https://github.com/astral-sh/ruff/pull/23538. That PR will update how we memoize the constraint sets that we build, with hand-rolled cached instead of relying on salsa interning. To do that, we need a create a new `ConstraintSetBuilder` type and thread it around through all of the various `has_relation_to` methods and friends. That's a slog to review, so I've pulled that out into this separate PR to make reviewing easier. This PR is a pure refactoring. There should be no behavioral changes. In particular, the new `ConstraintSetBuilder` type is currently empty! We're still using salsa interning at this stage of the migration, and so the builder doesn't need to hold onto any internal state. But don't worry, it will soon. --- crates/ty_python_semantic/src/types.rs | 133 ++- .../src/types/bound_super.rs | 70 +- .../ty_python_semantic/src/types/call/bind.rs | 260 ++-- crates/ty_python_semantic/src/types/class.rs | 107 +- .../src/types/constraints.rs | 288 +++-- crates/ty_python_semantic/src/types/cyclic.rs | 13 +- .../ty_python_semantic/src/types/function.rs | 17 +- .../ty_python_semantic/src/types/generics.rs | 237 ++-- .../src/types/ide_support.rs | 16 +- .../src/types/infer/builder.rs | 84 +- .../ty_python_semantic/src/types/instance.rs | 84 +- .../ty_python_semantic/src/types/newtype.rs | 32 +- .../ty_python_semantic/src/types/overrides.rs | 4 +- .../src/types/protocol_class.rs | 232 ++-- .../ty_python_semantic/src/types/relation.rs | 1052 +++++++++++------ .../src/types/signatures.rs | 258 ++-- .../src/types/subclass_of.rs | 38 +- crates/ty_python_semantic/src/types/tuple.rs | 209 ++-- .../src/types/typed_dict.rs | 77 +- 19 files changed, 2077 insertions(+), 1134 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 97fb4d22b11ff..166e2a7bc2dda 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -50,7 +50,9 @@ use crate::types::builder::RecursivelyDefined; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; use crate::types::class::NamedTupleSpec; pub(crate) use crate::types::class_base::ClassBase; -use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; +use crate::types::constraints::{ + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, OwnedConstraintSet, +}; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; @@ -1670,9 +1672,10 @@ impl<'db> Type<'db> { target: Type<'db>, inferable: InferableTypeVars<'_, 'db>, ) -> Type<'db> { + let constraints = ConstraintSetBuilder::new(); self.filter_union(db, |elem| { !elem - .when_disjoint_from(db, target, inferable) + .when_disjoint_from(db, target, &constraints, inferable) .is_always_satisfied(db) }) } @@ -2046,11 +2049,13 @@ impl<'db> Type<'db> { where F: FnMut(BoundTypeVarInstance<'db>, Type<'db>, TypeVarVariance, TypeContext<'db>), { + let constraints = ConstraintSetBuilder::new(); self.visit_specialization_impl( db, tcx, TypeVarVariance::Covariant, &mut f, + &constraints, &SpecializationVisitor::default(), ); } @@ -2061,24 +2066,44 @@ impl<'db> Type<'db> { tcx: TypeContext<'db>, polarity: TypeVarVariance, f: &mut dyn FnMut(BoundTypeVarInstance<'db>, Type<'db>, TypeVarVariance, TypeContext<'db>), + constraints: &ConstraintSetBuilder<'db>, visitor: &SpecializationVisitor<'db>, ) { let Type::NominalInstance(instance) = self else { match self { Type::Union(union) => { for element in union.elements(db) { - element.visit_specialization_impl(db, tcx, polarity, f, visitor); + element.visit_specialization_impl( + db, + tcx, + polarity, + f, + constraints, + visitor, + ); } } Type::Intersection(intersection) => { for element in intersection.positive(db) { - element.visit_specialization_impl(db, tcx, polarity, f, visitor); + element.visit_specialization_impl( + db, + tcx, + polarity, + f, + constraints, + visitor, + ); } } Type::TypeAlias(alias) => visitor.visit(self, || { - alias - .value_type(db) - .visit_specialization_impl(db, tcx, polarity, f, visitor); + alias.value_type(db).visit_specialization_impl( + db, + tcx, + polarity, + f, + constraints, + visitor, + ); }), _ => {} } @@ -2100,7 +2125,7 @@ impl<'db> Type<'db> { if let Some(tcx) = tcx.annotation { let alias_instance = Type::instance(db, class_literal.identity_specialization(db)); - let _ = builder.infer_reverse(tcx, alias_instance); + let _ = builder.infer_reverse(constraints, tcx, alias_instance); } builder.into_type_mappings() @@ -2113,7 +2138,7 @@ impl<'db> Type<'db> { f(type_var, *ty, variance, narrowed_tcx); visitor.visit(*ty, || { - ty.visit_specialization_impl(db, narrowed_tcx, variance, f, visitor); + ty.visit_specialization_impl(db, narrowed_tcx, variance, f, constraints, visitor); }); } } @@ -3675,8 +3700,9 @@ impl<'db> Type<'db> { } Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => { - let constraints = tracked_set.constraints(db); - Truthiness::from(constraints.is_always_satisfied(db)) + let constraints = ConstraintSetBuilder::new(); + let tracked_set = constraints.load(tracked_set.constraints(db)); + Truthiness::from(tracked_set.is_always_satisfied(db)) } Type::FunctionLiteral(_) @@ -4914,9 +4940,16 @@ impl<'db> Type<'db> { db: &'db dyn Db, argument_types: &CallArguments<'_, 'db>, ) -> Result, CallError<'db>> { + let constraints = ConstraintSetBuilder::new(); self.bindings(db) .match_parameters(db, argument_types) - .check_types(db, argument_types, TypeContext::default(), &[]) + .check_types( + db, + &constraints, + argument_types, + TypeContext::default(), + &[], + ) } /// Look up a dunder method on the meta-type of `self` and call it. @@ -5001,10 +5034,11 @@ impl<'db> Type<'db> { definedness: boundness, .. }) => { + let constraints = ConstraintSetBuilder::new(); let bindings = dunder_callable .bindings(db) .match_parameters(db, argument_types) - .check_types(db, argument_types, tcx, &[])?; + .check_types(db, &constraints, argument_types, tcx, &[])?; if boundness == Definedness::PossiblyUndefined { return Err(CallDunderError::PossiblyUnbound(Box::new(bindings))); @@ -5035,10 +5069,11 @@ impl<'db> Type<'db> { definedness: boundness, .. }) => { + let constraints = ConstraintSetBuilder::new(); let bindings = dunder_callable .bindings(db) .match_parameters(db, argument_types) - .check_types(db, argument_types, tcx, &[])?; + .check_types(db, &constraints, argument_types, tcx, &[])?; if boundness == Definedness::PossiblyUndefined { return Err(CallDunderError::PossiblyUnbound(Box::new(bindings))); @@ -7208,7 +7243,8 @@ impl<'db> TypeMapping<'_, 'db> { /// so out of an abundance of caution, we are interning the struct. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct InternedConstraintSet<'db> { - constraints: ConstraintSet<'db>, + #[returns(ref)] + constraints: OwnedConstraintSet<'db>, } // The Salsa heap is tracked separately. @@ -10301,15 +10337,17 @@ impl<'db> BoundMethodType<'db> { )) } - fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { // A bound method is a typically a subtype of itself. However, we must explicitly verify // the subtyping of the underlying function signatures (since they might be specialized // differently), and of the bound self parameter (taking care that parameters, including a @@ -10318,15 +10356,17 @@ impl<'db> BoundMethodType<'db> { .has_relation_to_impl( db, other.function(db), + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { other.self_instance(db).has_relation_to_impl( db, self.self_instance(db), + constraints, inferable, relation, relation_visitor, @@ -10528,22 +10568,25 @@ impl<'db> CallableType<'db> { /// Check whether this callable type has the given relation to another callable type. /// /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. - fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { if other.is_function_like(db) && !self.is_function_like(db) { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } self.signatures(db).has_relation_to_impl( db, other.signatures(db), + constraints, inferable, relation, relation_visitor, @@ -10600,19 +10643,22 @@ impl<'db> CallableTypes<'db> { Self::from_elements(self.0.iter().map(|element| f(*element))) } - pub(crate) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(crate) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: CallableType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { - self.0.iter().when_all(db, |element| { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + self.0.iter().when_all(db, constraints, |element| { element.has_relation_to_impl( db, other, + constraints, inferable, relation, relation_visitor, @@ -10690,15 +10736,17 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size } impl<'db> KnownBoundMethodType<'db> { - fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match (self, other) { ( KnownBoundMethodType::FunctionTypeDunderGet(self_function), @@ -10706,6 +10754,7 @@ impl<'db> KnownBoundMethodType<'db> { ) => self_function.has_relation_to_impl( db, other_function, + constraints, inferable, relation, relation_visitor, @@ -10718,6 +10767,7 @@ impl<'db> KnownBoundMethodType<'db> { ) => self_function.has_relation_to_impl( db, other_function, + constraints, inferable, relation, relation_visitor, @@ -10734,12 +10784,13 @@ impl<'db> KnownBoundMethodType<'db> { ) => Type::PropertyInstance(self_property).when_equivalent_to_impl( db, Type::PropertyInstance(other_property), + constraints, relation_visitor, disjointness_visitor, ), (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { - ConstraintSet::from(self == other) + ConstraintSet::from_bool(constraints, self == other) } ( @@ -10769,7 +10820,7 @@ impl<'db> KnownBoundMethodType<'db> { | ( KnownBoundMethodType::GenericContextSpecializeConstrained(_), KnownBoundMethodType::GenericContextSpecializeConstrained(_), - ) => ConstraintSet::from(true), + ) => ConstraintSet::from_bool(constraints, true), ( KnownBoundMethodType::FunctionTypeDunderGet(_) @@ -10796,7 +10847,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetSatisfies(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::GenericContextSpecializeConstrained(_), - ) => ConstraintSet::from(false), + ) => ConstraintSet::from_bool(constraints, false), } } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index f55dc2d5722ba..f850a628bd4e0 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -11,7 +11,7 @@ use crate::{ BoundTypeVarInstance, ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, MemberLookupPolicy, NominalInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, Type, TypeVarBoundOrConstraints, TypeVarConstraints, TypeVarInstance, UnionBuilder, - constraints::ConstraintSet, + constraints::{ConstraintSet, ConstraintSetBuilder}, context::InferContext, diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS}, relation::{HasRelationToVisitor, IsDisjointVisitor}, @@ -747,73 +747,89 @@ impl<'db> BoundSuperType<'db> { /// cannot simply delegate to `Type::is_equivalent_to_impl` for this /// case, because `Type::is_equivalent_to_impl` itself delegates back to /// `Type::has_relation_to_impl`, which would cause an infinite loop. - pub(crate) fn is_equivalent_to_impl( + pub(crate) fn is_equivalent_to_impl<'c>( self, db: &'db dyn Db, other: Self, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + constraints: &'c ConstraintSetBuilder<'db>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { let mut class_equivalence = match (self.pivot_class(db), other.pivot_class(db)) { (ClassBase::Class(left), ClassBase::Class(right)) => Type::from(left) .when_equivalent_to_impl( db, Type::from(right), + constraints, relation_visitor, disjointness_visitor, ), - (ClassBase::Class(_), _) => ConstraintSet::from(false), + (ClassBase::Class(_), _) => ConstraintSet::from_bool(constraints, false), // A `Divergent` type is only equivalent to itself ( ClassBase::Dynamic(DynamicType::Divergent(l)), ClassBase::Dynamic(DynamicType::Divergent(r)), - ) => ConstraintSet::from(l == r), + ) => ConstraintSet::from_bool(constraints, l == r), (ClassBase::Dynamic(DynamicType::Divergent(_)), _) - | (_, ClassBase::Dynamic(DynamicType::Divergent(_))) => ConstraintSet::from(false), - (ClassBase::Dynamic(_), ClassBase::Dynamic(_)) => ConstraintSet::from(true), - (ClassBase::Dynamic(_), _) => ConstraintSet::from(false), + | (_, ClassBase::Dynamic(DynamicType::Divergent(_))) => { + ConstraintSet::from_bool(constraints, false) + } + (ClassBase::Dynamic(_), ClassBase::Dynamic(_)) => { + ConstraintSet::from_bool(constraints, true) + } + (ClassBase::Dynamic(_), _) => ConstraintSet::from_bool(constraints, false), - (ClassBase::Generic, ClassBase::Generic) => ConstraintSet::from(true), - (ClassBase::Generic, _) => ConstraintSet::from(false), + (ClassBase::Generic, ClassBase::Generic) => ConstraintSet::from_bool(constraints, true), + (ClassBase::Generic, _) => ConstraintSet::from_bool(constraints, false), - (ClassBase::Protocol, ClassBase::Protocol) => ConstraintSet::from(true), - (ClassBase::Protocol, _) => ConstraintSet::from(false), + (ClassBase::Protocol, ClassBase::Protocol) => { + ConstraintSet::from_bool(constraints, true) + } + (ClassBase::Protocol, _) => ConstraintSet::from_bool(constraints, false), - (ClassBase::TypedDict, ClassBase::TypedDict) => ConstraintSet::from(true), - (ClassBase::TypedDict, _) => ConstraintSet::from(false), + (ClassBase::TypedDict, ClassBase::TypedDict) => { + ConstraintSet::from_bool(constraints, true) + } + (ClassBase::TypedDict, _) => ConstraintSet::from_bool(constraints, false), }; if class_equivalence.is_never_satisfied(db) { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } let owner_equivalence = match (self.owner(db), other.owner(db)) { (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => Type::from(left) .when_equivalent_to_impl( db, Type::from(right), + constraints, relation_visitor, disjointness_visitor, ), - (SuperOwnerKind::Class(_), _) => ConstraintSet::from(false), + (SuperOwnerKind::Class(_), _) => ConstraintSet::from_bool(constraints, false), (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => Type::from(left) .when_equivalent_to_impl( db, Type::from(right), + constraints, relation_visitor, disjointness_visitor, ), - (SuperOwnerKind::Instance(_), _) => ConstraintSet::from(false), + (SuperOwnerKind::Instance(_), _) => ConstraintSet::from_bool(constraints, false), // A `Divergent` type is only equivalent to itself ( SuperOwnerKind::Dynamic(DynamicType::Divergent(l)), SuperOwnerKind::Dynamic(DynamicType::Divergent(r)), - ) => ConstraintSet::from(l == r), + ) => ConstraintSet::from_bool(constraints, l == r), (SuperOwnerKind::Dynamic(DynamicType::Divergent(_)), _) - | (_, SuperOwnerKind::Dynamic(DynamicType::Divergent(_))) => ConstraintSet::from(false), - (SuperOwnerKind::Dynamic(_), SuperOwnerKind::Dynamic(_)) => ConstraintSet::from(true), - (SuperOwnerKind::Dynamic(_), _) => ConstraintSet::from(false), + | (_, SuperOwnerKind::Dynamic(DynamicType::Divergent(_))) => { + ConstraintSet::from_bool(constraints, false) + } + (SuperOwnerKind::Dynamic(_), SuperOwnerKind::Dynamic(_)) => { + ConstraintSet::from_bool(constraints, true) + } + (SuperOwnerKind::Dynamic(_), _) => ConstraintSet::from_bool(constraints, false), ( SuperOwnerKind::InstanceTypeVar(l_typevar, l_class), @@ -826,21 +842,23 @@ impl<'db> BoundSuperType<'db> { .when_equivalent_to_impl( db, Type::TypeVar(r_typevar), + constraints, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { Type::from(l_class).when_equivalent_to_impl( db, Type::from(r_class), + constraints, relation_visitor, disjointness_visitor, ) }), (SuperOwnerKind::InstanceTypeVar(..) | SuperOwnerKind::ClassTypeVar(..), _) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } }; - class_equivalence.intersect(db, owner_equivalence) + class_equivalence.intersect(db, constraints, owner_equivalence) } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 9fea57b4d7d25..b3a7b4b73b720 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -25,7 +25,7 @@ use crate::db::Db; use crate::dunder_all::dunder_all_names; use crate::place::{DefinedPlace, Definedness, Place, known_module_symbol}; use crate::types::call::arguments::{Expansion, is_expandable_type}; -use crate::types::constraints::ConstraintSet; +use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; use crate::types::diagnostic::{ CALL_NON_CALLABLE, CALL_TOP_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, INVALID_DATACLASS, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, @@ -89,13 +89,16 @@ impl<'db> BindingsElement<'db> { fn check_types( &mut self, db: &'db dyn Db, + constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, ) -> Option { let mut result = ArgumentForms::default(); let mut any_forms = false; for binding in &mut self.bindings { - if let Some(forms) = binding.check_types(db, argument_types, call_expression_tcx) { + if let Some(forms) = + binding.check_types(db, constraints, argument_types, call_expression_tcx) + { result.merge(&forms); any_forms = true; } @@ -426,12 +429,14 @@ impl<'db> Bindings<'db> { pub(crate) fn check_types( mut self, db: &'db dyn Db, + constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, dataclass_field_specifiers: &[Type<'db>], ) -> Result> { match self.check_types_impl( db, + constraints, argument_types, call_expression_tcx, dataclass_field_specifiers, @@ -444,6 +449,7 @@ impl<'db> Bindings<'db> { pub(crate) fn check_types_impl( &mut self, db: &'db dyn Db, + constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, dataclass_field_specifiers: &[Type<'db>], @@ -451,7 +457,7 @@ impl<'db> Bindings<'db> { // Check types for each element (union variant) for element in &mut self.elements { if let Some(updated_argument_forms) = - element.check_types(db, argument_types, call_expression_tcx) + element.check_types(db, constraints, argument_types, call_expression_tcx) { // If this element returned a new set of argument forms (indicating successful // argument type expansion), merge them into the existing forms. @@ -1146,8 +1152,11 @@ impl<'db> Bindings<'db> { Type::FunctionLiteral(function_type) => match function_type.known(db) { Some(KnownFunction::IsEquivalentTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let constraints = ty_a.when_equivalent_to(db, *ty_b); - let tracked = InternedConstraintSet::new(db, constraints); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ty_a.when_equivalent_to(db, *ty_b, constraints) + }); + let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), )); @@ -1156,9 +1165,16 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsSubtypeOf) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let constraints = - ty_a.when_subtype_of(db, *ty_b, InferableTypeVars::None); - let tracked = InternedConstraintSet::new(db, constraints); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ty_a.when_subtype_of( + db, + *ty_b, + constraints, + InferableTypeVars::None, + ) + }); + let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), )); @@ -1167,9 +1183,16 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsAssignableTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let constraints = - ty_a.when_assignable_to(db, *ty_b, InferableTypeVars::None); - let tracked = InternedConstraintSet::new(db, constraints); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ty_a.when_assignable_to( + db, + *ty_b, + constraints, + InferableTypeVars::None, + ) + }); + let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), )); @@ -1178,9 +1201,16 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsDisjointFrom) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let constraints = - ty_a.when_disjoint_from(db, *ty_b, InferableTypeVars::None); - let tracked = InternedConstraintSet::new(db, constraints); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ty_a.when_disjoint_from( + db, + *ty_b, + constraints, + InferableTypeVars::None, + ) + }); + let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), )); @@ -1723,8 +1753,17 @@ impl<'db> Bindings<'db> { else { return; }; - let constraints = ConstraintSet::range(db, *lower, *typevar, *upper); - let tracked = InternedConstraintSet::new(db, constraints); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ConstraintSet::constrain_typevar( + db, + constraints, + *typevar, + *lower, + *upper, + ) + }); + let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), )); @@ -1734,8 +1773,10 @@ impl<'db> Bindings<'db> { if !overload.parameter_types().is_empty() { return; } - let constraints = ConstraintSet::from(true); - let tracked = InternedConstraintSet::new(db, constraints); + let constraints = ConstraintSetBuilder::new(); + let result = constraints + .into_owned(|constraints| ConstraintSet::from_bool(constraints, true)); + let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), )); @@ -1745,8 +1786,10 @@ impl<'db> Bindings<'db> { if !overload.parameter_types().is_empty() { return; } - let constraints = ConstraintSet::from(false); - let tracked = InternedConstraintSet::new(db, constraints); + let constraints = ConstraintSetBuilder::new(); + let result = constraints + .into_owned(|constraints| ConstraintSet::from_bool(constraints, false)); + let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), )); @@ -1759,12 +1802,16 @@ impl<'db> Bindings<'db> { continue; }; - let result = ty_a.when_subtype_of_assuming( - db, - *ty_b, - tracked.constraints(db), - InferableTypeVars::None, - ); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ty_a.when_subtype_of_assuming( + db, + *ty_b, + constraints.load(tracked.constraints(db)), + constraints, + InferableTypeVars::None, + ) + }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), @@ -1782,9 +1829,12 @@ impl<'db> Bindings<'db> { continue; }; - let result = tracked - .constraints(db) - .implies(db, || other.constraints(db)); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + let lhs = constraints.load(tracked.constraints(db)); + let rhs = constraints.load(other.constraints(db)); + lhs.implies(db, constraints, || rhs) + }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( KnownInstanceType::ConstraintSet(tracked), @@ -1821,25 +1871,26 @@ impl<'db> Bindings<'db> { _ => continue, }; - let result = tracked - .constraints(db) - .satisfied_by_all_typevars(db, InferableTypeVars::One(&inferable)); + let constraints = ConstraintSetBuilder::new(); + let set = constraints.load(tracked.constraints(db)); + let result = + set.satisfied_by_all_typevars(db, InferableTypeVars::One(&inferable)); overload.set_return_type(Type::bool_literal(result)); } Type::KnownBoundMethod( KnownBoundMethodType::GenericContextSpecializeConstrained(generic_context), ) => { - let [Some(constraints)] = overload.parameter_types() else { + let [Some(set)] = overload.parameter_types() else { continue; }; - let Type::KnownInstance(KnownInstanceType::ConstraintSet(constraints)) = - constraints - else { + let Type::KnownInstance(KnownInstanceType::ConstraintSet(set)) = set else { continue; }; + let constraints = ConstraintSetBuilder::new(); + let set = constraints.load(set.constraints(db)); let specialization = - generic_context.specialize_constrained(db, constraints.constraints(db)); + generic_context.specialize_constrained(db, &constraints, set); let result = match specialization { Ok(specialization) => Type::KnownInstance( KnownInstanceType::Specialization(specialization), @@ -2097,6 +2148,7 @@ impl<'db> CallableBinding<'db> { fn check_types( &mut self, db: &'db dyn Db, + constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, ) -> Option { @@ -2124,7 +2176,12 @@ impl<'db> CallableBinding<'db> { // still perform type checking for non-overloaded function to provide better user // experience. if let [overload] = self.overloads.as_mut_slice() { - overload.check_types(db, argument_types.as_ref(), call_expression_tcx); + overload.check_types( + db, + constraints, + argument_types.as_ref(), + call_expression_tcx, + ); } return None; } @@ -2132,7 +2189,12 @@ impl<'db> CallableBinding<'db> { // If only one candidate overload remains, it is the winning match. Evaluate it as // a regular (non-overloaded) call. self.matching_overload_before_type_checking = Some(index); - self.overloads[index].check_types(db, argument_types.as_ref(), call_expression_tcx); + self.overloads[index].check_types( + db, + constraints, + argument_types.as_ref(), + call_expression_tcx, + ); return None; } MatchingOverloadIndex::Multiple(indexes) => { @@ -2144,7 +2206,12 @@ impl<'db> CallableBinding<'db> { // Step 2: Evaluate each remaining overload as a regular (non-overloaded) call to determine // whether it is compatible with the supplied argument list. for (_, overload) in self.matching_overloads_mut() { - overload.check_types(db, argument_types.as_ref(), call_expression_tcx); + overload.check_types( + db, + constraints, + argument_types.as_ref(), + call_expression_tcx, + ); } tracing::trace!( @@ -2186,6 +2253,7 @@ impl<'db> CallableBinding<'db> { // If two or more candidate overloads remain, proceed to step 5. self.filter_overloads_using_any_or_unknown( db, + constraints, argument_types.as_ref(), &indexes, ); @@ -2232,7 +2300,12 @@ impl<'db> CallableBinding<'db> { let parameter_type = overload.signature.parameters()[*parameter_index].annotated_type(); if argument_type - .when_assignable_to(db, parameter_type, overload.inferable_typevars) + .when_assignable_to( + db, + parameter_type, + constraints, + overload.inferable_typevars, + ) .is_always_satisfied(db) { is_argument_assignable_to_any_overload = true; @@ -2305,7 +2378,7 @@ impl<'db> CallableBinding<'db> { merged_argument_forms.merge(&argument_forms); for (_, overload) in self.matching_overloads_mut() { - overload.check_types(db, expanded_arguments, call_expression_tcx); + overload.check_types(db, constraints, expanded_arguments, call_expression_tcx); } tracing::trace!( @@ -2339,6 +2412,7 @@ impl<'db> CallableBinding<'db> { MatchingOverloadIndex::Multiple(indexes) => { self.filter_overloads_using_any_or_unknown( db, + constraints, expanded_arguments, &indexes, ); @@ -2449,6 +2523,7 @@ impl<'db> CallableBinding<'db> { fn filter_overloads_using_any_or_unknown( &mut self, db: &'db dyn Db, + constraints: &ConstraintSetBuilder<'db>, arguments: &CallArguments<'_, 'db>, matching_overload_indexes: &[usize], ) { @@ -2481,7 +2556,10 @@ impl<'db> CallableBinding<'db> { overload.signature.parameters()[parameter_index].annotated_type(); let first_parameter_type = &mut first_parameter_types[parameter_index]; if let Some(first_parameter_type) = first_parameter_type { - if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { + if !first_parameter_type + .when_equivalent_to(db, current_parameter_type, constraints) + .is_always_satisfied(db) + { participating_parameter_indexes.insert(parameter_index); } } else { @@ -2622,7 +2700,8 @@ impl<'db> CallableBinding<'db> { matching_overloads.all(|(_, overload)| { overload .return_type() - .is_equivalent_to(db, first_overload_return_type) + .when_equivalent_to(db, first_overload_return_type, constraints) + .is_always_satisfied(db) }) } else { // No matching overload @@ -3547,7 +3626,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { }) } - fn infer_specialization(&mut self) { + fn infer_specialization(&mut self, constraints: &ConstraintSetBuilder<'db>) { let Some(generic_context) = self.signature.generic_context else { return; }; @@ -3575,32 +3654,37 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .class_specialization(self.db)?; builder - .infer_reverse_map(tcx, return_ty, |(identity, variance, inferred_ty)| { - // Avoid unnecessarily widening the return type based on a covariant - // type parameter from the type context, as it can lead to argument - // assignability errors if the type variable is constrained by a narrower - // parameter type. - if variance.is_covariant() { - return None; - } + .infer_reverse_map( + constraints, + tcx, + return_ty, + |(identity, variance, inferred_ty)| { + // Avoid unnecessarily widening the return type based on a covariant + // type parameter from the type context, as it can lead to argument + // assignability errors if the type variable is constrained by a narrower + // parameter type. + if variance.is_covariant() { + return None; + } - // Avoid inferring a preferred type based on partially specialized type context - // from an outer generic call. If the type context is a union, we try to keep - // any concrete elements. - let inferred_ty = inferred_ty.filter_union(self.db, |ty| { - if ty.has_unspecialized_type_var(self.db) { - partially_specialized_declared_type.insert(identity); - false - } else { - true + // Avoid inferring a preferred type based on partially specialized type context + // from an outer generic call. If the type context is a union, we try to keep + // any concrete elements. + let inferred_ty = inferred_ty.filter_union(self.db, |ty| { + if ty.has_unspecialized_type_var(self.db) { + partially_specialized_declared_type.insert(identity); + false + } else { + true + } + }); + if inferred_ty.has_unspecialized_type_var(self.db) { + return None; } - }); - if inferred_ty.has_unspecialized_type_var(self.db) { - return None; - } - Some(inferred_ty) - }) + Some(inferred_ty) + }, + ) .ok()?; Some(builder.type_mappings().clone()) @@ -3609,6 +3693,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let mut specialization_errors = Vec::new(); let assignable_to_declared_type = self.infer_argument_types( + constraints, &mut builder, &preferred_type_mappings, &partially_specialized_declared_type, @@ -3625,6 +3710,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { specialization_errors.clear(); self.infer_argument_types( + constraints, &mut builder, &FxHashMap::default(), &FxHashSet::default(), @@ -3691,6 +3777,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn infer_argument_types( &mut self, + constraints: &ConstraintSetBuilder<'db>, builder: &mut SpecializationBuilder<'db>, preferred_type_mappings: &FxHashMap, Type<'db>>, partially_specialized_declared_type: &FxHashSet>, @@ -3706,6 +3793,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { self.argument_matches[argument_index].iter() { let specialization_result = builder.infer_map( + constraints, parameters[parameter_index].annotated_type(), variadic_argument_type.unwrap_or(argument_type), |(identity, _, inferred_ty)| { @@ -3741,6 +3829,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn check_argument_type( &mut self, + constraints: &ConstraintSetBuilder<'db>, argument_index: usize, adjusted_argument_index: Option, argument: Argument<'a>, @@ -3771,7 +3860,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { if !self.constraint_set_errors[argument_index] && !parameter.has_starred_annotation() && argument_type - .when_assignable_to(self.db, expected_ty, self.inferable_typevars) + .when_assignable_to(self.db, expected_ty, constraints, self.inferable_typevars) .is_never_satisfied(self.db) { let positional = matches!(argument, Argument::Positional | Argument::Synthetic) @@ -3794,7 +3883,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } } - fn check_argument_types(&mut self) { + fn check_argument_types(&mut self, constraints: &ConstraintSetBuilder<'db>) { let paramspec = self .signature .parameters() @@ -3804,7 +3893,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { self.enumerate_argument_types() { if let Some((_, paramspec)) = paramspec { - if self.try_paramspec_evaluation_at(argument_index, paramspec) { + if self.try_paramspec_evaluation_at(constraints, argument_index, paramspec) { // Once we find an argument that matches the `ParamSpec`, we can stop checking // the remaining arguments since `ParamSpec` should always be the last // parameter. @@ -3814,11 +3903,13 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { match argument { Argument::Variadic => self.check_variadic_argument_type( + constraints, argument_index, adjusted_argument_index, argument, ), Argument::Keywords => self.check_keyword_variadic_argument_type( + constraints, argument_index, adjusted_argument_index, argument, @@ -3828,6 +3919,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // If the argument isn't splatted, just check its type directly. for parameter_index in &self.argument_matches[argument_index].parameters { self.check_argument_type( + constraints, argument_index, adjusted_argument_index, argument, @@ -3856,7 +3948,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // // Here, no arguments match the `ParamSpec` parameter, but `P` specializes to `(x: int)`, // so we need to perform a sub-call with no arguments. - self.evaluate_paramspec_sub_call(None, paramspec); + self.evaluate_paramspec_sub_call(constraints, None, paramspec); } } @@ -3892,6 +3984,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { /// Returns `true` if the sub-call was invoked, `false` otherwise. fn try_paramspec_evaluation_at( &mut self, + constraints: &ConstraintSetBuilder<'db>, argument_index: usize, paramspec: BoundTypeVarInstance<'db>, ) -> bool { @@ -3907,7 +4000,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return false; } - self.evaluate_paramspec_sub_call(Some(argument_index), paramspec) + self.evaluate_paramspec_sub_call(constraints, Some(argument_index), paramspec) } /// Invoke a sub-call for the given `ParamSpec` type variable, using the remaining arguments. @@ -3921,6 +4014,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { /// For more details, refer to [`Self::try_paramspec_evaluation_at`]. fn evaluate_paramspec_sub_call( &mut self, + constraints: &ConstraintSetBuilder<'db>, argument_index: Option, paramspec: BoundTypeVarInstance<'db>, ) -> bool { @@ -3960,8 +4054,13 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { CallableBinding::from_overloads(self.signature_type, signatures.iter().cloned()); let bindings = match Bindings::from(callable_binding) .match_parameters(self.db, &sub_arguments) - .check_types(self.db, &sub_arguments, self.call_expression_tcx, &[]) - { + .check_types( + self.db, + constraints, + &sub_arguments, + self.call_expression_tcx, + &[], + ) { Ok(bindings) => bindings, Err(CallError(_, bindings)) => *bindings, }; @@ -4033,6 +4132,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn check_variadic_argument_type( &mut self, + constraints: &ConstraintSetBuilder<'db>, argument_index: usize, adjusted_argument_index: Option, argument: Argument<'a>, @@ -4041,6 +4141,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { self.argument_matches[argument_index].iter() { self.check_argument_type( + constraints, argument_index, adjusted_argument_index, argument, @@ -4052,6 +4153,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn check_keyword_variadic_argument_type( &mut self, + constraints: &ConstraintSetBuilder<'db>, argument_index: usize, adjusted_argument_index: Option, argument: Argument<'a>, @@ -4065,6 +4167,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .zip(&self.argument_matches[argument_index].parameters) { self.check_argument_type( + constraints, argument_index, adjusted_argument_index, argument, @@ -4080,6 +4183,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .when_assignable_to( self.db, KnownClass::Str.to_instance(self.db), + constraints, self.inferable_typevars, ) .is_always_satisfied(self.db) @@ -4107,6 +4211,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { std::iter::repeat(value_type).zip(&self.argument_matches[argument_index].parameters) { self.check_argument_type( + constraints, argument_index, adjusted_argument_index, Argument::Keywords, @@ -4279,6 +4384,7 @@ impl<'db> Binding<'db> { fn check_types( &mut self, db: &'db dyn Db, + constraints: &ConstraintSetBuilder<'db>, arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, ) { @@ -4302,8 +4408,8 @@ impl<'db> Binding<'db> { // If this overload is generic, first see if we can infer a specialization of the function // from the arguments that were passed in. - checker.infer_specialization(); - checker.check_argument_types(); + checker.infer_specialization(constraints); + checker.check_argument_types(constraints); (self.inferable_typevars, self.specialization, self.return_ty) = checker.finish(); } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index f6ee46ee5a45d..c02e75e7e1227 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -17,7 +17,9 @@ use crate::semantic_index::{ DeclarationWithConstraint, SemanticIndex, attribute_declarations, attribute_scopes, }; use crate::types::bound_super::BoundSuperError; -use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; +use crate::types::constraints::{ + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, +}; use crate::types::context::InferContext; use crate::types::diagnostic::{INVALID_DATACLASS_OVERRIDE, SUPER_CALL_IN_NAMED_TUPLE_METHOD}; use crate::types::enums::{ @@ -1113,77 +1115,88 @@ impl<'db> ClassType<'db> { /// Return `true` if `other` is present in this class's MRO. pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.when_subclass_of(db, other, InferableTypeVars::None) - .is_always_satisfied(db) + self.when_subclass_of( + db, + other, + &ConstraintSetBuilder::new(), + InferableTypeVars::None, + ) + .is_always_satisfied(db) } - pub(super) fn when_subclass_of( + pub(super) fn when_subclass_of<'c>( self, db: &'db dyn Db, other: ClassType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { + ) -> ConstraintSet<'db, 'c> { self.has_relation_to_impl( db, other, + constraints, inferable, TypeRelation::Subtyping, - &HasRelationToVisitor::default(), - &IsDisjointVisitor::default(), + &HasRelationToVisitor::default(constraints), + &IsDisjointVisitor::default(constraints), ) } - pub(super) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(super) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { - self.iter_mro(db).when_any(db, |base| { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + self.iter_mro(db).when_any(db, constraints, |base| { match base { ClassBase::Dynamic(_) => match relation { TypeRelation::Subtyping | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming(_) => { - ConstraintSet::from(other.is_object(db)) + | TypeRelation::SubtypingAssuming => { + ConstraintSet::from_bool(constraints, other.is_object(db)) } TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { - ConstraintSet::from(!other.is_final(db)) + ConstraintSet::from_bool(constraints, !other.is_final(db)) } }, // Protocol, Generic, and TypedDict are special bases that don't match ClassType. ClassBase::Protocol | ClassBase::Generic | ClassBase::TypedDict => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } ClassBase::Class(base) => match (base, other) { // Two non-generic classes match if they have the same class literal. (ClassType::NonGeneric(base_literal), ClassType::NonGeneric(other_literal)) => { - ConstraintSet::from(base_literal == other_literal) + ConstraintSet::from_bool(constraints, base_literal == other_literal) } // Two generic classes match if they have the same origin and compatible specializations. (ClassType::Generic(base), ClassType::Generic(other)) => { - ConstraintSet::from(base.origin(db) == other.origin(db)).and(db, || { - base.specialization(db).has_relation_to_impl( - db, - other.specialization(db), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + ConstraintSet::from_bool(constraints, base.origin(db) == other.origin(db)) + .and(db, constraints, || { + base.specialization(db).has_relation_to_impl( + db, + other.specialization(db), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) } // Generic and non-generic classes don't match. (ClassType::Generic(_), ClassType::NonGeneric(_)) | (ClassType::NonGeneric(_), ClassType::Generic(_)) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } }, } @@ -1212,7 +1225,12 @@ impl<'db> ClassType<'db> { } /// Return `true` if this class could exist in the MRO of `other`. - pub(super) fn could_exist_in_mro_of(self, db: &'db dyn Db, other: Self) -> bool { + pub(super) fn could_exist_in_mro_of( + self, + db: &'db dyn Db, + other: Self, + constraints: &ConstraintSetBuilder<'db>, + ) -> bool { other .iter_mro(db) .filter_map(ClassBase::into_class) @@ -1227,6 +1245,7 @@ impl<'db> ClassType<'db> { .is_disjoint_from( db, other_alias.specialization(db), + constraints, InferableTypeVars::None, ) .is_always_satisfied(db) @@ -1241,17 +1260,22 @@ impl<'db> ClassType<'db> { /// For two given classes `A` and `B`, it is often possible to say for sure /// that there could never exist any class `C` that inherits from both `A` and `B`. /// In these situations, this method returns `false`; in all others, it returns `true`. - pub(super) fn could_coexist_in_mro_with(self, db: &'db dyn Db, other: Self) -> bool { + pub(super) fn could_coexist_in_mro_with( + self, + db: &'db dyn Db, + other: Self, + constraints: &ConstraintSetBuilder<'db>, + ) -> bool { if self == other { return true; } if self.is_final(db) { - return other.could_exist_in_mro_of(db, self); + return other.could_exist_in_mro_of(db, self, constraints); } if other.is_final(db) { - return self.could_exist_in_mro_of(db, other); + return self.could_exist_in_mro_of(db, other, constraints); } // Two disjoint bases can only coexist in an MRO if one is a subclass of the other. @@ -1288,7 +1312,15 @@ impl<'db> ClassType<'db> { let Some(other_metaclass_instance) = other_metaclass.to_instance(db) else { return true; }; - if self_metaclass_instance.is_disjoint_from(db, other_metaclass_instance) { + if self_metaclass_instance + .when_disjoint_from( + db, + other_metaclass_instance, + constraints, + InferableTypeVars::None, + ) + .is_always_satisfied(db) + { return false; } @@ -7477,12 +7509,13 @@ impl KnownClass { .is_ok_and(|class| class.is_subclass_of(db, None, other)) } - pub(super) fn when_subclass_of<'db>( + pub(super) fn when_subclass_of<'db, 'c>( self, db: &'db dyn Db, other: ClassType<'db>, - ) -> ConstraintSet<'db> { - ConstraintSet::from(self.is_subclass_of(db, other)) + constraints: &'c ConstraintSetBuilder<'db>, + ) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(constraints, self.is_subclass_of(db, other)) } /// Return the module in which we should look up the definition for this class diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index d182a228f0d45..95e1d4067585a 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -68,7 +68,8 @@ use std::cell::RefCell; use std::cmp::Ordering; -use std::fmt::Display; +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; use std::ops::Range; use indexmap::map::Entry; @@ -92,25 +93,45 @@ use crate::{Db, FxIndexMap, FxIndexSet, FxOrderSet}; pub(crate) trait OptionConstraintsExtension { /// Returns a constraint set that is always satisfiable if the option is `None`; otherwise /// applies a function to determine under what constraints the value inside of it holds. - fn when_none_or<'db>(self, f: impl FnOnce(T) -> ConstraintSet<'db>) -> ConstraintSet<'db>; + fn when_none_or<'db, 'c>( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + f: impl FnOnce(T) -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c>; /// Returns a constraint set that is never satisfiable if the option is `None`; otherwise /// applies a function to determine under what constraints the value inside of it holds. - fn when_some_and<'db>(self, f: impl FnOnce(T) -> ConstraintSet<'db>) -> ConstraintSet<'db>; + fn when_some_and<'db, 'c>( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + f: impl FnOnce(T) -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c>; } impl OptionConstraintsExtension for Option { - fn when_none_or<'db>(self, f: impl FnOnce(T) -> ConstraintSet<'db>) -> ConstraintSet<'db> { + fn when_none_or<'db, 'c>( + self, + _db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + f: impl FnOnce(T) -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match self { Some(value) => f(value), - None => ConstraintSet::always(), + None => ConstraintSet::always(builder), } } - fn when_some_and<'db>(self, f: impl FnOnce(T) -> ConstraintSet<'db>) -> ConstraintSet<'db> { + fn when_some_and<'db, 'c>( + self, + _db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + f: impl FnOnce(T) -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match self { Some(value) => f(value), - None => ConstraintSet::never(), + None => ConstraintSet::never(builder), } } } @@ -122,47 +143,72 @@ pub(crate) trait IteratorConstraintsExtension { /// This method short-circuits; if we encounter any element that /// [`is_always_satisfied`][ConstraintSet::is_always_satisfied], then the overall result /// must be as well, and we stop consuming elements from the iterator. - fn when_any<'db>( + fn when_any<'db, 'c>( self, db: &'db dyn Db, - f: impl FnMut(T) -> ConstraintSet<'db>, - ) -> ConstraintSet<'db>; + builder: &'c ConstraintSetBuilder<'db>, + f: impl FnMut(T) -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c>; /// Returns the constraints under which every element of the iterator holds. /// /// This method short-circuits; if we encounter any element that /// [`is_never_satisfied`][ConstraintSet::is_never_satisfied], then the overall result /// must be as well, and we stop consuming elements from the iterator. - fn when_all<'db>( + fn when_all<'db, 'c>( self, db: &'db dyn Db, - f: impl FnMut(T) -> ConstraintSet<'db>, - ) -> ConstraintSet<'db>; + builder: &'c ConstraintSetBuilder<'db>, + f: impl FnMut(T) -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c>; } impl IteratorConstraintsExtension for I where I: Iterator, { - fn when_any<'db>( + fn when_any<'db, 'c>( self, db: &'db dyn Db, - mut f: impl FnMut(T) -> ConstraintSet<'db>, - ) -> ConstraintSet<'db> { - let node = Node::distributed_or(db, self.map(|element| f(element).node)); - ConstraintSet { node } + builder: &'c ConstraintSetBuilder<'db>, + mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + let node = Node::distributed_or( + db, + self.map(|element| { + let constraint = f(element); + constraint.verify_builder(builder); + constraint.node + }), + ); + ConstraintSet::from_node(builder, node) } - fn when_all<'db>( + fn when_all<'db, 'c>( self, db: &'db dyn Db, - mut f: impl FnMut(T) -> ConstraintSet<'db>, - ) -> ConstraintSet<'db> { - let node = Node::distributed_and(db, self.map(|element| f(element).node)); - ConstraintSet { node } + builder: &'c ConstraintSetBuilder<'db>, + mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + let node = Node::distributed_and( + db, + self.map(|element| { + let constraint = f(element); + constraint.verify_builder(builder); + constraint.node + }), + ); + ConstraintSet::from_node(builder, node) } } +#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] +pub struct OwnedConstraintSet<'db> { + /// The BDD representing this constraint set + node: Node<'db>, + storage: ConstraintSetStorage<'db>, +} + /// A set of constraints under which a type property holds. /// /// This is called a "set of constraint sets", and denoted _𝒮_, in [[POPL2015][]]. @@ -173,35 +219,56 @@ where /// [`or`][Self::or] in a consistent order. /// /// [POPL2015]: https://doi.org/10.1145/2676726.2676991 -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] -pub struct ConstraintSet<'db> { +#[derive(Clone, Copy)] +pub struct ConstraintSet<'db, 'c> { /// The BDD representing this constraint set node: Node<'db>, + builder: &'c ConstraintSetBuilder<'db>, + _invariant: PhantomData &'c ()>, } -impl<'db> ConstraintSet<'db> { - fn never() -> Self { +impl<'db, 'c> ConstraintSet<'db, 'c> { + fn from_node(builder: &'c ConstraintSetBuilder<'db>, node: Node<'db>) -> Self { Self { - node: Node::AlwaysFalse, + node, + builder, + _invariant: PhantomData, } } - fn always() -> Self { - Self { - node: Node::AlwaysTrue, + fn never(builder: &'c ConstraintSetBuilder<'db>) -> Self { + Self::from_node(builder, Node::AlwaysFalse) + } + + fn always(builder: &'c ConstraintSetBuilder<'db>) -> Self { + Self::from_node(builder, Node::AlwaysTrue) + } + + pub(crate) fn from_bool(builder: &'c ConstraintSetBuilder<'db>, b: bool) -> Self { + if b { + Self::always(builder) + } else { + Self::never(builder) } } /// Returns a constraint set that constraints a typevar to a particular range of types. pub(crate) fn constrain_typevar( db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, upper: Type<'db>, ) -> Self { - Self { - node: ConstrainedTypeVar::new_node(db, typevar, lower, upper), - } + Self::from_node( + builder, + ConstrainedTypeVar::new_node(db, typevar, lower, upper), + ) + } + + #[track_caller] + fn verify_builder(self, builder: &'c ConstraintSetBuilder<'db>) { + debug_assert!(std::ptr::eq(self.builder, builder)); } /// Returns whether this constraint set never holds @@ -327,12 +394,12 @@ impl<'db> ConstraintSet<'db> { pub(crate) fn implies_subtype_of( self, db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, lhs: Type<'db>, rhs: Type<'db>, ) -> Self { - Self { - node: self.node.implies_subtype_of(db, lhs, rhs), - } + self.verify_builder(builder); + Self::from_node(builder, self.node.implies_subtype_of(db, lhs, rhs)) } /// Returns whether this constraint set is satisfied by all of the typevars that it mentions. @@ -350,7 +417,7 @@ impl<'db> ConstraintSet<'db> { /// means that those additional typevars trivially satisfy the constraint set, regardless of /// whether they are inferable or not. pub(crate) fn satisfied_by_all_typevars( - self, + &self, db: &'db dyn Db, inferable: InferableTypeVars<'_, 'db>, ) -> bool { @@ -361,7 +428,13 @@ impl<'db> ConstraintSet<'db> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self { + pub(crate) fn union( + &mut self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + other: Self, + ) -> Self { + self.verify_builder(builder); self.node = self.node.or_with_offset(db, other.node); *self } @@ -370,16 +443,21 @@ impl<'db> ConstraintSet<'db> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn intersect(&mut self, db: &'db dyn Db, other: Self) -> Self { + pub(crate) fn intersect( + &mut self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + other: Self, + ) -> Self { + self.verify_builder(builder); self.node = self.node.and_with_offset(db, other.node); *self } /// Returns the negation of this constraint set. - pub(crate) fn negate(self, db: &'db dyn Db) -> Self { - Self { - node: self.node.negate(db), - } + pub(crate) fn negate(self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>) -> Self { + self.verify_builder(builder); + Self::from_node(builder, self.node.negate(db)) } /// Returns the intersection of this constraint set and another. The other constraint set is @@ -388,9 +466,17 @@ impl<'db> ConstraintSet<'db> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn and(mut self, db: &'db dyn Db, other: impl FnOnce() -> Self) -> Self { + pub(crate) fn and( + mut self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + other: impl FnOnce() -> Self, + ) -> Self { + self.verify_builder(builder); if !self.is_never_satisfied(db) { - self.intersect(db, other()); + let other = other(); + other.verify_builder(builder); + self.intersect(db, builder, other); } self } @@ -401,9 +487,17 @@ impl<'db> ConstraintSet<'db> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn or(mut self, db: &'db dyn Db, other: impl FnOnce() -> Self) -> Self { + pub(crate) fn or( + mut self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + other: impl FnOnce() -> Self, + ) -> Self { + self.verify_builder(builder); if !self.is_always_satisfied(db) { - self.union(db, other()); + let other = other(); + other.verify_builder(builder); + self.union(db, builder, other); } self } @@ -412,18 +506,27 @@ impl<'db> ConstraintSet<'db> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn implies(self, db: &'db dyn Db, other: impl FnOnce() -> Self) -> Self { - self.negate(db).or(db, other) + pub(crate) fn implies( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + other: impl FnOnce() -> Self, + ) -> Self { + self.negate(db, builder).or(db, builder, other) } /// Returns a constraint set encoding that this constraint set is equivalent to another. /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn iff(self, db: &'db dyn Db, other: Self) -> Self { - ConstraintSet { - node: self.node.iff_with_offset(db, other.node), - } + pub(crate) fn iff( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + other: Self, + ) -> Self { + self.verify_builder(builder); + Self::from_node(builder, self.node.iff_with_offset(db, other.node)) } /// Reduces the set of inferable typevars for this constraint set. You provide an iterator of @@ -434,10 +537,11 @@ impl<'db> ConstraintSet<'db> { pub(crate) fn reduce_inferable( self, db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, to_remove: impl IntoIterator>, ) -> Self { - let node = self.node.exists(db, to_remove); - Self { node } + self.verify_builder(builder); + Self::from_node(builder, self.node.exists(db, to_remove)) } pub(crate) fn solutions(self, db: &'db dyn Db) -> Solutions<'db> { @@ -450,15 +554,6 @@ impl<'db> ConstraintSet<'db> { self.node.solutions(db) } - pub(crate) fn range( - db: &'db dyn Db, - lower: Type<'db>, - typevar: BoundTypeVarInstance<'db>, - upper: Type<'db>, - ) -> Self { - Self::constrain_typevar(db, typevar, lower, upper) - } - #[expect(dead_code)] // Keep this around for debugging purposes pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { self.node.simplify_for_display(db).display(db) @@ -470,9 +565,47 @@ impl<'db> ConstraintSet<'db> { } } -impl From for ConstraintSet<'_> { - fn from(b: bool) -> Self { - if b { Self::always() } else { Self::never() } +impl Debug for ConstraintSet<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConstraintSet") + .field("node", &self.node) + .finish() + } +} + +#[derive(Default)] +pub(crate) struct ConstraintSetBuilder<'db> { + storage: RefCell>, +} + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] +struct ConstraintSetStorage<'db> { + _dummy: PhantomData<&'db ()>, +} + +impl<'db> ConstraintSetBuilder<'db> { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn into_owned( + self, + f: impl for<'c> FnOnce(&'c Self) -> ConstraintSet<'db, 'c>, + ) -> OwnedConstraintSet<'db> { + let constraint = f(&self); + let node = constraint.node; + OwnedConstraintSet { + node, + storage: self.storage.into_inner(), + } + } + + pub(crate) fn load<'c>(&'c self, other: &OwnedConstraintSet<'db>) -> ConstraintSet<'db, 'c> { + // For now, all constraints are still salsa-interned globally, so we can just coerce the + // constraint set to consider ourselves as where it's stored. Once we migrate to actually + // storing the constraints in ConstraintSetStorage, this will need to copy the relevant BDD + // nodes from other's storage into ourselves. + ConstraintSet::from_node(self, other.node) } } @@ -1873,7 +2006,7 @@ impl<'db> Node<'db> { Node::Interior(_) => { let mut clauses = self.node.satisfied_clauses(self.db); clauses.simplify(self.db); - clauses.display(self.db).fmt(f) + Display::fmt(&clauses.display(self.db), f) } } } @@ -4167,10 +4300,11 @@ impl<'db> BoundTypeVarInstance<'db> { } impl<'db> GenericContext<'db> { - pub(crate) fn specialize_constrained( + pub(crate) fn specialize_constrained<'c>( self, db: &'db dyn Db, - constraints: ConstraintSet<'db>, + _builder: &'c ConstraintSetBuilder<'db>, + constraints: ConstraintSet<'db, 'c>, ) -> Result, ()> { tracing::trace!( target: "ty_python_semantic::types::constraints::specialize_constrained", @@ -4345,13 +4479,15 @@ mod tests { BoundTypeVarInstance::synthetic(&db, Name::new_static("U"), TypeVarVariance::Invariant); let bool_type = KnownClass::Bool.to_instance(&db); let str_type = KnownClass::Str.to_instance(&db); - let t_str = ConstraintSet::range(&db, str_type, t, str_type); - let t_bool = ConstraintSet::range(&db, bool_type, t, bool_type); - let u_str = ConstraintSet::range(&db, str_type, u, str_type); - let u_bool = ConstraintSet::range(&db, bool_type, u, bool_type); + let constraints = ConstraintSetBuilder::new(); + let t_str = ConstraintSet::constrain_typevar(&db, &constraints, t, str_type, str_type); + let t_bool = ConstraintSet::constrain_typevar(&db, &constraints, t, bool_type, bool_type); + let u_str = ConstraintSet::constrain_typevar(&db, &constraints, u, str_type, str_type); + let u_bool = ConstraintSet::constrain_typevar(&db, &constraints, u, bool_type, bool_type); // Construct this in a different order than above to make the source_orders more // interesting. - let constraints = (u_str.or(&db, || u_bool)).and(&db, || t_str.or(&db, || t_bool)); + let constraints = (u_str.or(&db, &constraints, || u_bool)) + .and(&db, &constraints, || t_str.or(&db, &constraints, || t_bool)); let actual = constraints.node.display_graph(&db, &"").to_string(); assert_eq!(actual, expected); } diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index 6f179b1a72f1c..d279f0f93c207 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -60,7 +60,7 @@ impl Default for TypeTransformer<'_, Tag> { pub(crate) type PairVisitor<'db, Tag, C> = CycleDetector, Type<'db>), C>; #[derive(Debug)] -pub struct CycleDetector { +pub struct CycleDetector { /// If the type we're visiting is present in `seen`, it indicates that we've hit a cycle (due /// to a recursive type); we need to immediately short circuit the whole operation and return /// the fallback value. That's why we pop items off the end of `seen` after we've visited them. @@ -80,16 +80,25 @@ pub struct CycleDetector { fallback: R, + pub(crate) extra: Extra, + _tag: PhantomData, } -impl CycleDetector { +impl CycleDetector { pub fn new(fallback: R) -> Self { + Self::with_extra(fallback, Extra::default()) + } +} + +impl CycleDetector { + pub(crate) fn with_extra(fallback: R, extra: Extra) -> Self { CycleDetector { seen: RefCell::new(FxIndexSet::default()), cache: RefCell::new(FxHashMap::default()), depth: Cell::new(0), fallback, + extra, _tag: PhantomData, } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index f735af5e273f7..9e14bf373e3c0 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -65,7 +65,7 @@ use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{FileScopeId, SemanticIndex, semantic_index}; use crate::types::call::{Binding, CallArguments}; -use crate::types::constraints::ConstraintSet; +use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; use crate::types::context::InferContext; use crate::types::diagnostic::{ ASSERT_TYPE_UNSPELLABLE_SUBTYPE, INVALID_ARGUMENT_TYPE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, @@ -1187,17 +1187,19 @@ impl<'db> FunctionType<'db> { BoundMethodType::new(db, self, self_instance) } - pub(crate) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(crate) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { if self.literal(db) != other.literal(db) { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } let self_signature = self.signature(db); @@ -1205,6 +1207,7 @@ impl<'db> FunctionType<'db> { self_signature.has_relation_to_impl( db, other_signature, + constraints, inferable, relation, relation_visitor, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 75df307bf439f..4bde75f353e4e 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -14,7 +14,9 @@ use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKey, NodeWithScopeK use crate::semantic_index::{SemanticIndex, semantic_index}; use crate::types::class::ClassType; use crate::types::class_base::ClassBase; -use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension, Solutions}; +use crate::types::constraints::{ + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, Solutions, +}; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Parameters}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; @@ -989,16 +991,17 @@ pub(super) fn walk_specialization<'db, V: TypeVisitor<'db> + ?Sized>( } #[expect(clippy::too_many_arguments)] -fn is_subtype_in_invariant_position<'db>( +fn is_subtype_in_invariant_position<'db, 'c>( db: &'db dyn Db, derived_type: &Type<'db>, derived_materialization: MaterializationKind, base_type: &Type<'db>, base_materialization: MaterializationKind, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, -) -> ConstraintSet<'db> { + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, +) -> ConstraintSet<'db, 'c> { let derived_top = derived_type.top_materialization(db); let derived_bottom = derived_type.bottom_materialization(db); let base_top = base_type.top_materialization(db); @@ -1009,17 +1012,18 @@ fn is_subtype_in_invariant_position<'db>( // This should be removed and properly handled in the respective // `(Type::TypeVar(_), _) | (_, Type::TypeVar(_))` branch of // `Type::has_relation_to_impl`. Right now, we cannot generally - // return `ConstraintSet::from(true)` from that branch, as that + // return `ConstraintSet::from_bool(constraints,true)` from that branch, as that // leads to union simplification, which means that we lose track // of type variables without recording the constraints under which // the relation holds. if matches!(base, Type::TypeVar(_)) || matches!(derived, Type::TypeVar(_)) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } derived.has_relation_to_impl( db, base, + constraints, inferable, TypeRelation::Subtyping, relation_visitor, @@ -1031,12 +1035,12 @@ fn is_subtype_in_invariant_position<'db>( // is a subset of the range covered by `Base`. (MaterializationKind::Top, MaterializationKind::Top) => { is_subtype_of(base_bottom, derived_bottom) - .and(db, || is_subtype_of(derived_top, base_top)) + .and(db, constraints, || is_subtype_of(derived_top, base_top)) } // One bottom is a subtype of another if it covers a strictly larger set of materializations. (MaterializationKind::Bottom, MaterializationKind::Bottom) => { is_subtype_of(derived_bottom, base_bottom) - .and(db, || is_subtype_of(base_top, derived_top)) + .and(db, constraints, || is_subtype_of(base_top, derived_top)) } // The bottom materialization of `Derived` is a subtype of the top materialization // of `Base` if there is some type that is both within the @@ -1044,21 +1048,21 @@ fn is_subtype_in_invariant_position<'db>( // exists, it's a subtype of `Top[base]` and a supertype of `Bottom[derived]`. (MaterializationKind::Bottom, MaterializationKind::Top) => { is_subtype_of(base_bottom, derived_bottom) - .and(db, || is_subtype_of(derived_bottom, base_top)) - .or(db, || { + .and(db, constraints, || is_subtype_of(derived_bottom, base_top)) + .or(db, constraints, || { is_subtype_of(base_bottom, derived_top) - .and(db, || is_subtype_of(derived_top, base_top)) + .and(db, constraints, || is_subtype_of(derived_top, base_top)) }) - .or(db, || { + .or(db, constraints, || { is_subtype_of(base_top, derived_top) - .and(db, || is_subtype_of(derived_bottom, base_top)) + .and(db, constraints, || is_subtype_of(derived_bottom, base_top)) }) } // A top materialization is a subtype of a bottom materialization only if both original // un-materialized types are the same fully static type. (MaterializationKind::Top, MaterializationKind::Bottom) => { is_subtype_of(derived_top, base_bottom) - .and(db, || is_subtype_of(base_top, derived_bottom)) + .and(db, constraints, || is_subtype_of(base_top, derived_bottom)) } } } @@ -1067,17 +1071,18 @@ fn is_subtype_in_invariant_position<'db>( /// have a relation (subtyping or assignability), taking into account /// that the two types may come from a top or bottom materialization. #[expect(clippy::too_many_arguments)] -fn has_relation_in_invariant_position<'db>( +fn has_relation_in_invariant_position<'db, 'c>( db: &'db dyn Db, derived_type: &Type<'db>, derived_materialization: Option, base_type: &Type<'db>, base_materialization: Option, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, -) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, +) -> ConstraintSet<'db, 'c> { match (derived_materialization, base_materialization, relation) { // Top and bottom materializations are fully static types, so subtyping // is the same as assignability. @@ -1087,6 +1092,7 @@ fn has_relation_in_invariant_position<'db>( derived_mat, base_type, base_mat, + constraints, inferable, relation_visitor, disjointness_visitor, @@ -1107,15 +1113,17 @@ fn has_relation_in_invariant_position<'db>( .has_relation_to_impl( db, *base_type, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { base_type.has_relation_to_impl( db, *derived_type, + constraints, inferable, relation, relation_visitor, @@ -1128,13 +1136,14 @@ fn has_relation_in_invariant_position<'db>( Some(base_mat), TypeRelation::Subtyping | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming(_), + | TypeRelation::SubtypingAssuming, ) => is_subtype_in_invariant_position( db, derived_type, MaterializationKind::Top, base_type, base_mat, + constraints, inferable, relation_visitor, disjointness_visitor, @@ -1144,13 +1153,14 @@ fn has_relation_in_invariant_position<'db>( None, TypeRelation::Subtyping | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming(_), + | TypeRelation::SubtypingAssuming, ) => is_subtype_in_invariant_position( db, derived_type, derived_mat, base_type, MaterializationKind::Bottom, + constraints, inferable, relation_visitor, disjointness_visitor, @@ -1166,6 +1176,7 @@ fn has_relation_in_invariant_position<'db>( MaterializationKind::Bottom, base_type, base_mat, + constraints, inferable, relation_visitor, disjointness_visitor, @@ -1180,6 +1191,7 @@ fn has_relation_in_invariant_position<'db>( derived_mat, base_type, MaterializationKind::Top, + constraints, inferable, relation_visitor, disjointness_visitor, @@ -1462,18 +1474,20 @@ impl<'db> Specialization<'db> { ) } - pub(crate) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(crate) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { let generic_context = self.generic_context(db); if generic_context != other.generic_context(db) { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db)) @@ -1481,6 +1495,7 @@ impl<'db> Specialization<'db> { return self_tuple.has_relation_to_impl( db, other_tuple, + constraints, inferable, relation, relation_visitor, @@ -1497,7 +1512,7 @@ impl<'db> Specialization<'db> { other.types(db) ); - types.when_all(db, |(bound_typevar, self_type, other_type)| { + types.when_all(db, constraints, |(bound_typevar, self_type, other_type)| { // Subtyping/assignability of each type in the specialization depends on the variance // of the corresponding typevar: // - covariant: verify that self_type <: other_type @@ -1511,6 +1526,7 @@ impl<'db> Specialization<'db> { self_materialization_kind, other_type, other_materialization_kind, + constraints, inferable, relation, relation_visitor, @@ -1519,6 +1535,7 @@ impl<'db> Specialization<'db> { TypeVarVariance::Covariant => self_type.has_relation_to_impl( db, *other_type, + constraints, inferable, relation, relation_visitor, @@ -1527,42 +1544,46 @@ impl<'db> Specialization<'db> { TypeVarVariance::Contravariant => other_type.has_relation_to_impl( db, *self_type, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ), - TypeVarVariance::Bivariant => ConstraintSet::from(true), + TypeVarVariance::Bivariant => ConstraintSet::from_bool(constraints, true), } }) } - pub(crate) fn is_disjoint_from( + pub(crate) fn is_disjoint_from<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { + ) -> ConstraintSet<'db, 'c> { self.is_disjoint_from_impl( db, other, + constraints, inferable, - &IsDisjointVisitor::default(), - &HasRelationToVisitor::default(), + &IsDisjointVisitor::default(constraints), + &HasRelationToVisitor::default(constraints), ) } - pub(crate) fn is_disjoint_from_impl( + pub(crate) fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - ) -> ConstraintSet<'db> { + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { let generic_context = self.generic_context(db); if generic_context != other.generic_context(db) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db)) @@ -1570,6 +1591,7 @@ impl<'db> Specialization<'db> { return self_tuple.is_disjoint_from_impl( db, other_tuple, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1584,6 +1606,7 @@ impl<'db> Specialization<'db> { types.when_all( db, + constraints, |(bound_typevar, self_type, other_type)| match bound_typevar.variance(db) { // TODO: This check can lead to false negatives. // @@ -1594,19 +1617,20 @@ impl<'db> Specialization<'db> { TypeVarVariance::Invariant => self_type.is_disjoint_from_impl( db, *other_type, + constraints, inferable, disjointness_visitor, relation_visitor, ), // If `Foo[T]` is covariant in `T`, `Foo[Never]` is a subtype of `Foo[A]` and `Foo[B]` - TypeVarVariance::Covariant => ConstraintSet::from(false), + TypeVarVariance::Covariant => ConstraintSet::from_bool(constraints, false), // If `Foo[T]` is contravariant in `T`, `Foo[A | B]` is a subtype of `Foo[A]` and `Foo[B]` - TypeVarVariance::Contravariant => ConstraintSet::from(false), + TypeVarVariance::Contravariant => ConstraintSet::from_bool(constraints, false), // If `Foo[T]` is bivariant in `T`, `Foo[A]` and `Foo[B]` are mutual subtypes. - TypeVarVariance::Bivariant => ConstraintSet::from(false), + TypeVarVariance::Bivariant => ConstraintSet::from_bool(constraints, false), }, ) } @@ -1799,10 +1823,10 @@ impl<'db> SpecializationBuilder<'db> { /// specialization directly from that constraint set. This method lets us migrate to that brave /// new world incrementally, by using the new constraint set mechanism piecemeal for certain /// type comparisons. - fn add_type_mappings_from_constraint_set( + fn add_type_mappings_from_constraint_set<'c>( &mut self, formal: Type<'db>, - constraints: ConstraintSet<'db>, + constraints: ConstraintSet<'db, 'c>, mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, ) -> Result<(), ()> { let solutions = match constraints.solutions(self.db) { @@ -1830,10 +1854,16 @@ impl<'db> SpecializationBuilder<'db> { let formal_is_single_paramspec = formal_signature.is_single_paramspec().is_some(); for actual_callable in actual_callables.as_slice() { + let constraints = ConstraintSetBuilder::new(); if formal_is_single_paramspec { let when = actual_callable .signatures(self.db) - .when_constraint_set_assignable_to(self.db, formal_signature, self.inferable); + .when_constraint_set_assignable_to( + self.db, + formal_signature, + &constraints, + self.inferable, + ); self.add_type_mappings_from_constraint_set(formal, when, &mut *f)?; } else { // An overloaded actual callable is compatible with the formal signature if at @@ -1844,6 +1874,7 @@ impl<'db> SpecializationBuilder<'db> { let when = actual_signature.when_constraint_set_assignable_to_signatures( self.db, formal_signature, + &constraints, self.inferable, ); if self @@ -1864,10 +1895,11 @@ impl<'db> SpecializationBuilder<'db> { /// Infer type mappings for the specialization based on a given type and its declared type. pub(crate) fn infer( &mut self, + constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, ) -> Result<(), SpecializationError<'db>> { - self.infer_map(formal, actual, |(_, _, ty)| Some(ty)) + self.infer_map(constraints, formal, actual, |(_, _, ty)| Some(ty)) } /// Infer type mappings for the specialization based on a given type and its declared type. @@ -1876,11 +1908,13 @@ impl<'db> SpecializationBuilder<'db> { /// optionally modify the inferred type, or filter out the type mapping entirely. pub(crate) fn infer_map( &mut self, + constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, ) -> Result<(), SpecializationError<'db>> { self.infer_map_impl( + constraints, formal, actual, TypeVarVariance::Covariant, @@ -1891,6 +1925,7 @@ impl<'db> SpecializationBuilder<'db> { fn infer_map_impl( &mut self, + constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, polarity: TypeVarVariance, @@ -1927,7 +1962,14 @@ impl<'db> SpecializationBuilder<'db> { // Expand PEP 695 type aliases in the formal type. // This is necessary for solving generics like `def head[T](my_list: MyList[T]) -> T`. (Type::TypeAlias(alias), _) => { - return self.infer_map_impl(alias.value_type(self.db), actual, polarity, f, seen); + return self.infer_map_impl( + constraints, + alias.value_type(self.db), + actual, + polarity, + f, + seen, + ); } // TODO: We haven't implemented a full unification solver yet. If typevars appear in @@ -1993,7 +2035,7 @@ impl<'db> SpecializationBuilder<'db> { if !actual.is_never() { let assignable_elements = union_formal.elements(self.db).iter().filter(|ty| { actual - .when_subtype_of(self.db, **ty, self.inferable) + .when_subtype_of(self.db, **ty, constraints, self.inferable) .is_always_satisfied(self.db) }); if assignable_elements.exactly_one().is_ok() { @@ -2023,15 +2065,26 @@ impl<'db> SpecializationBuilder<'db> { let mut first_error = None; let mut found_matching_element = false; for formal_element in union_formal.elements(self.db) { - let result = - self.infer_map_impl(*formal_element, actual, polarity, &mut f, seen); + let result = self.infer_map_impl( + constraints, + *formal_element, + actual, + polarity, + &mut f, + seen, + ); if let Err(err) = result { first_error.get_or_insert(err); } else { // The recursive call to `infer_map_impl` may succeed even if the actual type is // not assignable to the formal element. if !actual - .when_assignable_to(self.db, *formal_element, self.inferable) + .when_assignable_to( + self.db, + *formal_element, + constraints, + self.inferable, + ) .is_never_satisfied(self.db) { found_matching_element = true; @@ -2050,7 +2103,7 @@ impl<'db> SpecializationBuilder<'db> { // actual type must also be disjoint from every negative element of the // intersection, but that doesn't help us infer any type mappings.) for positive in formal.iter_positive(self.db) { - self.infer_map_impl(positive, actual, polarity, f, seen)?; + self.infer_map_impl(constraints, positive, actual, polarity, f, seen)?; } } @@ -2075,7 +2128,7 @@ impl<'db> SpecializationBuilder<'db> { return Ok(()); } if !ty - .when_assignable_to(self.db, bound, self.inferable) + .when_assignable_to(self.db, bound, constraints, self.inferable) .is_always_satisfied(self.db) { return Err(SpecializationError::MismatchedBound { @@ -2085,9 +2138,9 @@ impl<'db> SpecializationBuilder<'db> { } self.add_type_mapping(bound_typevar, ty, polarity, f); } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { // Prefer an exact match first. - for constraint in constraints.elements(self.db) { + for constraint in typevar_constraints.elements(self.db) { if ty == *constraint { self.add_type_mapping(bound_typevar, ty, polarity, f); return Ok(()); @@ -2112,13 +2165,12 @@ impl<'db> SpecializationBuilder<'db> { { let all_satisfied = actual_constraints.iter().all(|actual_constraint| { - constraints - .elements(self.db) - .iter() - .any(|formal_constraint| { + typevar_constraints.elements(self.db).iter().any( + |formal_constraint| { actual_constraint .is_equivalent_to(self.db, *formal_constraint) - }) + }, + ) }); if all_satisfied { self.add_type_mapping(bound_typevar, ty, polarity, f); @@ -2126,14 +2178,19 @@ impl<'db> SpecializationBuilder<'db> { } } - for constraint in constraints.elements(self.db) { + for constraint in typevar_constraints.elements(self.db) { let is_satisfied = if polarity.is_contravariant() { constraint - .when_assignable_to(self.db, ty, self.inferable) + .when_assignable_to(self.db, ty, constraints, self.inferable) .is_always_satisfied(self.db) } else { - ty.when_assignable_to(self.db, *constraint, self.inferable) - .is_always_satisfied(self.db) + ty.when_assignable_to( + self.db, + *constraint, + constraints, + self.inferable, + ) + .is_always_satisfied(self.db) }; if is_satisfied { @@ -2156,6 +2213,7 @@ impl<'db> SpecializationBuilder<'db> { let formal_instance = Type::TypeVar(subclass_of.into_type_var().unwrap()); if let Some(actual_instance) = ty.to_instance(self.db) { return self.infer_map_impl( + constraints, formal_instance, actual_instance, polarity, @@ -2172,7 +2230,14 @@ impl<'db> SpecializationBuilder<'db> { // Retry specialization with the literal's fallback instance so literals can // contribute to generic inference for nominal and protocol formals. let actual_instance = literal.fallback_instance(self.db); - return self.infer_map_impl(formal, actual_instance, polarity, f, seen); + return self.infer_map_impl( + constraints, + formal, + actual_instance, + polarity, + f, + seen, + ); } (formal, Type::ProtocolInstance(actual_protocol)) => { @@ -2183,6 +2248,7 @@ impl<'db> SpecializationBuilder<'db> { // infer the specialization of the protocol that the class implements. if let Some(actual_nominal) = actual_protocol.to_nominal_instance() { return self.infer_map_impl( + constraints, formal, Type::NominalInstance(actual_nominal), polarity, @@ -2216,6 +2282,7 @@ impl<'db> SpecializationBuilder<'db> { { let variance = TypeVarVariance::Covariant.compose(polarity); self.infer_map_impl( + constraints, *formal_element, *actual_element, variance, @@ -2240,6 +2307,7 @@ impl<'db> SpecializationBuilder<'db> { let when = actual.when_constraint_set_assignable_to( self.db, formal, + constraints, self.inferable, ); // For protocol inference via constraint sets, we currently treat @@ -2277,7 +2345,14 @@ impl<'db> SpecializationBuilder<'db> { base_specialization ) { let variance = typevar.variance_with_polarity(self.db, polarity); - self.infer_map_impl(*formal_ty, *base_ty, variance, &mut f, seen)?; + self.infer_map_impl( + constraints, + *formal_ty, + *base_ty, + variance, + &mut f, + seen, + )?; } return Ok(()); } @@ -2336,7 +2411,14 @@ impl<'db> SpecializationBuilder<'db> { // when it can be matched directly against a type variable in the formal type, // e.g., `reveal_type(alias)` should reveal the type alias, not its value type. (formal, Type::TypeAlias(alias)) => { - return self.infer_map_impl(formal, alias.value_type(self.db), polarity, f, seen); + return self.infer_map_impl( + constraints, + formal, + alias.value_type(self.db), + polarity, + f, + seen, + ); } // TODO: Add more forms that we can structurally induct into: type[C], callables @@ -2350,10 +2432,11 @@ impl<'db> SpecializationBuilder<'db> { /// actual type, not the formal type, contains inferable type variables. pub(crate) fn infer_reverse( &mut self, + constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, ) -> Result<(), SpecializationError<'db>> { - self.infer_reverse_map(formal, actual, |(_, _, ty)| Some(ty)) + self.infer_reverse_map(constraints, formal, actual, |(_, _, ty)| Some(ty)) } /// Infer type mappings for the specialization in the reverse direction, i.e., where the @@ -2363,11 +2446,13 @@ impl<'db> SpecializationBuilder<'db> { /// optionally modify the inferred type, or filter out the type mapping entirely. pub(crate) fn infer_reverse_map( &mut self, + constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, ) -> Result<(), SpecializationError<'db>> { self.infer_reverse_map_impl( + constraints, formal, actual, TypeVarVariance::Covariant, @@ -2378,6 +2463,7 @@ impl<'db> SpecializationBuilder<'db> { fn infer_reverse_map_impl( &mut self, + constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, polarity: TypeVarVariance, @@ -2411,7 +2497,7 @@ impl<'db> SpecializationBuilder<'db> { // Collect the actual type to which each synthetic type variable is mapped. let forward_type_mappings = { let mut builder = SpecializationBuilder::new(self.db, inferable); - builder.infer(synthetic_formal, actual)?; + builder.infer(constraints, synthetic_formal, actual)?; builder.into_type_mappings() }; @@ -2419,7 +2505,7 @@ impl<'db> SpecializationBuilder<'db> { // // This is the base case for when `actual` is an inferable type variable. if forward_type_mappings.is_empty() { - return self.infer_map_impl(actual, formal, polarity, f, seen); + return self.infer_map_impl(constraints, actual, formal, polarity, f, seen); } // Consider the reverse inference of `Sequence[int]` given `list[T]`. @@ -2434,7 +2520,14 @@ impl<'db> SpecializationBuilder<'db> { // Note that it is possible that we need to recurse deeper, so we continue // to perform a reverse inference on the nested types. - self.infer_reverse_map_impl(formal_type, *actual_type, variance, f, seen)?; + self.infer_reverse_map_impl( + constraints, + formal_type, + *actual_type, + variance, + f, + seen, + )?; } } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 29c3ee7476697..900cc0a9bcf13 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -6,6 +6,7 @@ use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::DefinitionKind; use crate::semantic_index::{attribute_scopes, global_scope, semantic_index, use_def_map}; use crate::types::call::{CallArguments, CallError, MatchedArgument}; +use crate::types::constraints::ConstraintSetBuilder; use crate::types::signatures::{ParameterKind, Signature}; use crate::types::{ CallDunderError, CallableTypes, ClassBase, ClassLiteral, ClassType, KnownUnion, Type, @@ -670,7 +671,14 @@ pub fn call_signature_details<'db>( // For example, calling `dict[str, int].get("a")` resolves the `_KT` // TypeVar to `str`. We ignore errors since we still want signature // details even if the call has type errors. - let _ = bindings.check_types_impl(db, &call_arguments, TypeContext::default(), &[]); + let constraints = ConstraintSetBuilder::new(); + let _ = bindings.check_types_impl( + db, + &constraints, + &call_arguments, + TypeContext::default(), + &[], + ); // Extract signature details from all callable bindings bindings @@ -721,9 +729,10 @@ pub fn call_type_simplified_by_overloads( }); // Try to resolve overloads with the arguments/types we have + let constraints = ConstraintSetBuilder::new(); let mut resolved = bindings .match_parameters(db, &args) - .check_types(db, &args, TypeContext::default(), &[]) + .check_types(db, &constraints, &args, TypeContext::default(), &[]) // Only use the Ok .iter() .flat_map(super::call::bind::Bindings::iter_flat) @@ -908,10 +917,11 @@ fn resolve_call_signature<'db>( }); // Extract the `Bindings` regardless of whether type checking succeeded or failed. + let constraints = ConstraintSetBuilder::new(); let bindings = callable_type .bindings(db) .match_parameters(db, &args) - .check_types(db, &args, TypeContext::default(), &[]) + .check_types(db, &constraints, &args, TypeContext::default(), &[]) .unwrap_or_else(|CallError(_, bindings)| *bindings); // First, try to find the matching overload after full type checking. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index cc66d51609d0e..45fd4c31202ad 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -67,6 +67,7 @@ use crate::types::class::{ DynamicMetaclassConflict, DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, FieldKind, MetaclassErrorKind, MethodDecorator, NamedTupleField, NamedTupleSpec, }; +use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; use crate::types::diagnostic::{ @@ -10630,6 +10631,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { call_expression_tcx: TypeContext<'db>, ) -> Result<(), CallErrorKind> { let db = self.db(); + let constraints = ConstraintSetBuilder::new(); let has_generic_context = bindings .iter_flat() @@ -10669,6 +10671,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if speculated_bindings .check_types_impl( db, + &constraints, argument_types, narrowed_tcx, &self.dataclass_field_specifiers, @@ -10708,6 +10711,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(bindings.check_types_impl( db, + &constraints, argument_types, narrowed_tcx, &self.dataclass_field_specifiers, @@ -10751,6 +10755,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bindings.check_types_impl( db, + &constraints, argument_types, call_expression_tcx, &self.dataclass_field_specifiers, @@ -10774,6 +10779,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { debug_assert_eq!(arguments_types.len(), bindings.argument_forms().len()); let db = self.db(); + let constraints = ConstraintSetBuilder::new(); let iter = itertools::izip!( 0.., arguments_types.iter_mut(), @@ -10858,6 +10864,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(declared_return_ty) = call_expression_tcx.annotation { let _ = builder.infer_reverse( + &constraints, declared_return_ty, overload .constructor_instance_type @@ -11656,6 +11663,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return None; }; + let constraints = ConstraintSetBuilder::new(); let inferable = generic_context.inferable_typevars(self.db()); // Remove any union elements of that are unrelated to the collection type. @@ -11689,6 +11697,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder .infer_reverse_map( + &constraints, tcx, collection_instance, |(typevar, variance, inferred_ty)| { @@ -11745,7 +11754,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // collection by unioning the inferred type with `Unknown`. let elt_tcx = elt_tcx.unwrap_or(Type::unknown()); - builder.infer(Type::TypeVar(elt_ty), elt_tcx).ok()?; + builder + .infer(&constraints, Type::TypeVar(elt_ty), elt_tcx) + .ok()?; } for elts in elts { @@ -11773,10 +11784,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut elt_tys = elt_tys.clone(); if let Some((key_ty, value_ty)) = elt_tys.next_tuple() { - builder.infer(Type::TypeVar(key_ty), unpacked_key_ty).ok()?; + builder + .infer(&constraints, Type::TypeVar(key_ty), unpacked_key_ty) + .ok()?; builder - .infer(Type::TypeVar(value_ty), unpacked_value_ty) + .infer(&constraints, Type::TypeVar(value_ty), unpacked_value_ty) .ok()?; } @@ -11823,6 +11836,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder .infer( + &constraints, Type::TypeVar(elt_ty), if elt.is_starred_expr() { inferred_elt_ty @@ -13943,12 +13957,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => fallback_unary_expression_type(), }, - ( - ast::UnaryOp::Invert, - Type::KnownInstance(KnownInstanceType::ConstraintSet(constraints)), - ) => { - let constraints = constraints.constraints(self.db()); - let result = constraints.negate(self.db()); + (ast::UnaryOp::Invert, Type::KnownInstance(KnownInstanceType::ConstraintSet(set))) => { + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + let set = constraints.load(set.constraints(self.db())); + set.negate(self.db(), constraints) + }); Type::KnownInstance(KnownInstanceType::ConstraintSet( InternedConstraintSet::new(self.db(), result), )) @@ -14703,9 +14717,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), ast::Operator::BitAnd, ) => { - let left = left.constraints(self.db()); - let right = right.constraints(self.db()); - let result = left.and(self.db(), || right); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + let left = constraints.load(left.constraints(self.db())); + let right = constraints.load(right.constraints(self.db())); + left.and(self.db(), constraints, || right) + }); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( InternedConstraintSet::new(self.db(), result), ))) @@ -14716,9 +14733,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), ast::Operator::BitOr, ) => { - let left = left.constraints(self.db()); - let right = right.constraints(self.db()); - let result = left.or(self.db(), || right); + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + let left = constraints.load(left.constraints(self.db())); + let right = constraints.load(right.constraints(self.db())); + left.or(self.db(), constraints, || right) + }); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( InternedConstraintSet::new(self.db(), result), ))) @@ -15568,14 +15588,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ( Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), - ) => match op { - ast::CmpOp::Eq => Some(Ok(Type::bool_literal( - left.constraints(self.db()).iff(self.db(), right.constraints(self.db())).is_always_satisfied(self.db()), - ))), - ast::CmpOp::NotEq => Some(Ok(Type::bool_literal( - !left.constraints(self.db()).iff(self.db(), right.constraints(self.db())).is_always_satisfied(self.db()), - ))), - _ => None, + ) => { + let constraints = ConstraintSetBuilder::new(); + let left = constraints.load(left.constraints(self.db())); + let right = constraints.load(right.constraints(self.db())); + let result = left.iff(self.db(), &constraints, right); + let equivalent = result.is_always_satisfied(self.db()); + match op { + ast::CmpOp::Eq => Some(Ok(Type::bool_literal(equivalent))), + ast::CmpOp::NotEq => Some(Ok(Type::bool_literal(!equivalent))), + _ => None, + } } ( @@ -16385,6 +16408,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let db = self.db(); + let constraints = ConstraintSetBuilder::new(); let slice_node = subscript.slice.as_ref(); let exactly_one_paramspec = generic_context.exactly_one_paramspec(db); @@ -16452,7 +16476,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match typevar.typevar(db).bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { if provided_type - .when_assignable_to(db, bound, InferableTypeVars::None) + .when_assignable_to( + db, + bound, + &constraints, + InferableTypeVars::None, + ) .is_never_satisfied(db) { let node = get_node(index); @@ -16474,7 +16503,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { specialization_types.push(Some(provided_type)); } } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { // TODO: this is wrong, the given specialization needs to be assignable // to _at least one_ of the individual constraints, not to the union of // all of them. `int | str` is not a valid specialization of a typevar @@ -16482,7 +16511,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if provided_type .when_assignable_to( db, - constraints.as_type(db), + typevar_constraints.as_type(db), + &constraints, InferableTypeVars::None, ) .is_never_satisfied(db) @@ -16495,7 +16525,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "Type `{}` does not satisfy constraints `{}` \ of type variable `{}`", provided_type.display(db), - constraints + typevar_constraints .elements(db) .iter() .map(|c| c.display(db)) diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 4873ee1fb07e5..48ec1a0a16191 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -11,7 +11,9 @@ use super::protocol_class::ProtocolInterface; use super::{BoundTypeVarInstance, ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance}; use crate::place::PlaceAndQualifiers; use crate::semantic_index::definition::Definition; -use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; +use crate::types::constraints::{ + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, +}; use crate::types::enums::is_single_member_enum; use crate::types::generics::{InferableTypeVars, walk_specialization}; use crate::types::protocol_class::{ProtocolClass, walk_protocol_interface}; @@ -137,20 +139,22 @@ impl<'db> Type<'db> { } /// Return `true` if `self` conforms to the interface described by `protocol`. - pub(super) fn satisfies_protocol( + #[expect(clippy::too_many_arguments)] + pub(super) fn satisfies_protocol<'c>( self, db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { // `self` might satisfy the protocol nominally, if `protocol` is a class-based protocol and // `self` has the protocol class in its MRO. This is a much cheaper check than the // structural check we perform below, so we do it first to avoid the structural check when // we can. - let mut result = ConstraintSet::from(false); + let mut result = ConstraintSet::from_bool(constraints, false); if let Some(nominal_instance) = protocol.to_nominal_instance() { // if `self` and `other` are *both* protocols, we also need to treat `self` as if it // were a nominal type, or we won't consider a protocol `P` that explicitly inherits @@ -164,13 +168,14 @@ impl<'db> Type<'db> { let nominally_satisfied = type_to_test.has_relation_to_impl( db, Type::NominalInstance(nominal_instance), + constraints, inferable, relation, relation_visitor, disjointness_visitor, ); if result - .union(db, nominally_satisfied) + .union(db, constraints, nominally_satisfied) .is_always_satisfied(db) { return result; @@ -195,6 +200,7 @@ impl<'db> Type<'db> { self_protocol.interface(db).has_relation_to_impl( db, protocol.interface(db), + constraints, inferable, relation, relation_visitor, @@ -205,10 +211,11 @@ impl<'db> Type<'db> { .inner .interface(db) .members(db) - .when_all(db, |member| { + .when_all(db, constraints, |member| { member.is_satisfied_by( db, self, + constraints, inferable, relation, relation_visitor, @@ -216,7 +223,7 @@ impl<'db> Type<'db> { ) }) }; - result.or(db, || structurally_satisfied) + result.or(db, constraints, || structurally_satisfied) } } @@ -430,23 +437,26 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(super) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match (self.0, other.0) { - (_, NominalInstanceInner::Object) => ConstraintSet::from(true), + (_, NominalInstanceInner::Object) => ConstraintSet::from_bool(constraints, true), ( NominalInstanceInner::ExactTuple(tuple1), NominalInstanceInner::ExactTuple(tuple2), ) => tuple1.has_relation_to_impl( db, tuple2, + constraints, inferable, relation, relation_visitor, @@ -455,6 +465,7 @@ impl<'db> NominalInstanceType<'db> { _ => self.class(db).has_relation_to_impl( db, other.class(db), + constraints, inferable, relation, relation_visitor, @@ -463,38 +474,44 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn is_disjoint_from_impl( + pub(super) fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - ) -> ConstraintSet<'db> { + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { if self.is_object() || other.is_object() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } - let mut result = ConstraintSet::from(false); + let mut result = ConstraintSet::from_bool(constraints, false); if let Some(self_spec) = self.tuple_spec(db) { if let Some(other_spec) = other.tuple_spec(db) { let compatible = self_spec.is_disjoint_from_impl( db, &other_spec, + constraints, inferable, disjointness_visitor, relation_visitor, ); - if result.union(db, compatible).is_always_satisfied(db) { + if result + .union(db, constraints, compatible) + .is_always_satisfied(db) + { return result; } } } - result.or(db, || { - ConstraintSet::from( + result.or(db, constraints, || { + ConstraintSet::from_bool( + constraints, !self .class(db) - .could_coexist_in_mro_with(db, other.class(db)), + .could_coexist_in_mro_with(db, other.class(db), constraints), ) }) } @@ -712,14 +729,16 @@ impl<'db> ProtocolInstanceType<'db> { protocol: ProtocolInstanceType<'db>, _: (), ) -> bool { + let constraints = ConstraintSetBuilder::new(); Type::object() .satisfies_protocol( db, protocol, + &constraints, InferableTypeVars::None, TypeRelation::Subtyping, - &HasRelationToVisitor::default(), - &IsDisjointVisitor::default(), + &HasRelationToVisitor::default(&constraints), + &IsDisjointVisitor::default(&constraints), ) .is_always_satisfied(db) } @@ -744,14 +763,15 @@ impl<'db> ProtocolInstanceType<'db> { /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y` /// have a member with the same name but disjoint types #[expect(clippy::unused_self)] - pub(super) fn is_disjoint_from_impl( + pub(super) fn is_disjoint_from_impl<'c>( self, _db: &'db dyn Db, _other: Self, + constraints: &'c ConstraintSetBuilder<'db>, _inferable: InferableTypeVars<'_, 'db>, - _visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { - ConstraintSet::from(false) + _visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(constraints, false) } pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index afd5fcfb314c0..08cc882ee4afb 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -1,6 +1,6 @@ use crate::Db; use crate::semantic_index::definition::{Definition, DefinitionKind}; -use crate::types::constraints::ConstraintSet; +use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; use crate::types::{ClassType, KnownUnion, Type, definition_expression_type, visitor}; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; @@ -117,27 +117,41 @@ impl<'db> NewType<'db> { // Since a regular class can't inherit from a newtype, the only way for one newtype to be a // subtype of another is to have the other in its chain of newtype bases. Once we reach the // base class, we don't have to keep looking. - pub(crate) fn has_relation_to_impl(self, db: &'db dyn Db, other: Self) -> ConstraintSet<'db> { + pub(crate) fn has_relation_to_impl<'c>( + self, + db: &'db dyn Db, + other: Self, + constraints: &'c ConstraintSetBuilder<'db>, + ) -> ConstraintSet<'db, 'c> { if self.is_equivalent_to_impl(db, other) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } for base in self.iter_bases(db) { if let NewTypeBase::NewType(base_newtype) = base { if base_newtype.is_equivalent_to_impl(db, other) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } } } - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } - pub(crate) fn is_disjoint_from_impl(self, db: &'db dyn Db, other: Self) -> ConstraintSet<'db> { + pub(crate) fn is_disjoint_from_impl<'c>( + self, + db: &'db dyn Db, + other: Self, + constraints: &'c ConstraintSetBuilder<'db>, + ) -> ConstraintSet<'db, 'c> { // Two NewTypes are disjoint if they're not equal and neither inherits from the other. // NewTypes have single inheritance, and a regular class can't inherit from a NewType, so // it's not possible for some third type to multiply-inherit from both. - let mut self_not_subtype_of_other = self.has_relation_to_impl(db, other).negate(db); - let other_not_subtype_of_self = other.has_relation_to_impl(db, self).negate(db); - self_not_subtype_of_other.intersect(db, other_not_subtype_of_self) + let mut self_not_subtype_of_other = self + .has_relation_to_impl(db, other, constraints) + .negate(db, constraints); + let other_not_subtype_of_self = other + .has_relation_to_impl(db, self, constraints) + .negate(db, constraints); + self_not_subtype_of_other.intersect(db, constraints, other_not_subtype_of_self) } /// Create a new `NewType` by mapping the underlying `ClassType`. This descends through any diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 5fbac45e8be75..83f13747e4ad7 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -26,6 +26,7 @@ use crate::{ StaticClassLiteral, Type, TypeContext, TypeQualifiers, call::CallArguments, class::{CodeGeneratorKind, FieldKind}, + constraints::ConstraintSetBuilder, context::InferContext, diagnostic::{ INVALID_ASSIGNMENT, INVALID_DATACLASS, INVALID_EXPLICIT_OVERRIDE, @@ -768,10 +769,11 @@ fn check_enum_member_against_init<'db>( let call_args = CallArguments::positional(args); let call_args = call_args.with_self(Some(self_type)); + let constraints = ConstraintSetBuilder::new(); let result = Type::FunctionLiteral(init_function) .bindings(db) .match_parameters(db, &call_args) - .check_types(db, &call_args, TypeContext::default(), &[]); + .check_types(db, &constraints, &call_args, TypeContext::default(), &[]); if result.is_err() { if let Some(builder) = context.report_lint( diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index dd8832898de7f..44d643d6610df 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -20,7 +20,10 @@ use crate::{ FindLegacyTypeVarsVisitor, InstanceFallbackShadowsNonDataDescriptor, KnownFunction, MemberLookupPolicy, PropertyInstanceType, Signature, StaticClassLiteral, Type, TypeMapping, TypeQualifiers, TypeVarVariance, VarianceInferable, - constraints::{ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension}, + constraints::{ + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, + OptionConstraintsExtension, + }, context::InferContext, diagnostic::report_undeclared_protocol_member, generics::InferableTypeVars, @@ -287,103 +290,116 @@ impl<'db> ProtocolInterface<'db> { .unwrap_or_else(|| Type::object().member(db, name)) } - pub(super) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(super) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { - other.members(db).when_all(db, |other_member| { - self.member_by_name(db, other_member.name) - .when_some_and(|our_member| match (our_member.kind, other_member.kind) { - // Method members are always immutable; - // they can never be subtypes of/assignable to mutable attribute members. - (ProtocolMemberKind::Method(_), ProtocolMemberKind::Other(_)) => { - ConstraintSet::from(false) - } + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + other.members(db).when_all(db, constraints, |other_member| { + self.member_by_name(db, other_member.name).when_some_and( + db, + constraints, + |our_member| { + match (our_member.kind, other_member.kind) { + // Method members are always immutable; + // they can never be subtypes of/assignable to mutable attribute members. + (ProtocolMemberKind::Method(_), ProtocolMemberKind::Other(_)) => { + ConstraintSet::from_bool(constraints, false) + } - // A property member can only be a subtype of an attribute member - // if the property is readable *and* writable. - // - // TODO: this should also consider the types of the members on both sides. - (ProtocolMemberKind::Property(property), ProtocolMemberKind::Other(_)) => { - ConstraintSet::from( - property.getter(db).is_some() && property.setter(db).is_some(), - ) - } + // A property member can only be a subtype of an attribute member + // if the property is readable *and* writable. + // + // TODO: this should also consider the types of the members on both sides. + (ProtocolMemberKind::Property(property), ProtocolMemberKind::Other(_)) => { + ConstraintSet::from_bool( + constraints, + property.getter(db).is_some() && property.setter(db).is_some(), + ) + } - // A `@property` member can never be a subtype of a method member, as it is not necessarily - // accessible on the meta-type, whereas a method member must be. - (ProtocolMemberKind::Property(_), ProtocolMemberKind::Method(_)) => { - ConstraintSet::from(false) - } + // A `@property` member can never be a subtype of a method member, as it is not necessarily + // accessible on the meta-type, whereas a method member must be. + (ProtocolMemberKind::Property(_), ProtocolMemberKind::Method(_)) => { + ConstraintSet::from_bool(constraints, false) + } - // But an attribute member *can* be a subtype of a method member, - // providing it is marked `ClassVar` - ( - ProtocolMemberKind::Other(our_type), - ProtocolMemberKind::Method(other_type), - ) => ConstraintSet::from( - our_member.qualifiers.contains(TypeQualifiers::CLASS_VAR), - ) - .and(db, || { - our_type.has_relation_to_impl( - db, - Type::Callable(protocol_bind_self(db, other_type, None)), - inferable, - relation, - relation_visitor, - disjointness_visitor, + // But an attribute member *can* be a subtype of a method member, + // providing it is marked `ClassVar` + ( + ProtocolMemberKind::Other(our_type), + ProtocolMemberKind::Method(other_type), + ) => ConstraintSet::from_bool( + constraints, + our_member.qualifiers.contains(TypeQualifiers::CLASS_VAR), ) - }), - - ( - ProtocolMemberKind::Method(our_method), - ProtocolMemberKind::Method(other_method), - ) => our_method.bind_self(db, None).has_relation_to_impl( - db, - protocol_bind_self(db, other_method, None), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), + .and(db, constraints, || { + our_type.has_relation_to_impl( + db, + Type::Callable(protocol_bind_self(db, other_type, None)), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }), - ( - ProtocolMemberKind::Other(our_type), - ProtocolMemberKind::Other(other_type), - ) => our_type - .has_relation_to_impl( + ( + ProtocolMemberKind::Method(our_method), + ProtocolMemberKind::Method(other_method), + ) => our_method.bind_self(db, None).has_relation_to_impl( db, - other_type, + protocol_bind_self(db, other_method, None), + constraints, inferable, relation, relation_visitor, disjointness_visitor, - ) - .and(db, || { - other_type.has_relation_to_impl( + ), + + ( + ProtocolMemberKind::Other(our_type), + ProtocolMemberKind::Other(other_type), + ) => our_type + .has_relation_to_impl( db, - our_type, + other_type, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - }), - - // TODO: finish assignability/subtyping between two `@property` members, - // and between a `@property` member and a member of a different kind. - ( - ProtocolMemberKind::Property(_) - | ProtocolMemberKind::Method(_) - | ProtocolMemberKind::Other(_), - ProtocolMemberKind::Property(_), - ) => ConstraintSet::from(true), - }) + .and(db, constraints, || { + other_type.has_relation_to_impl( + db, + our_type, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }), + + // TODO: finish assignability/subtyping between two `@property` members, + // and between a `@property` member and a member of a different kind. + ( + ProtocolMemberKind::Property(_) + | ProtocolMemberKind::Method(_) + | ProtocolMemberKind::Other(_), + ProtocolMemberKind::Property(_), + ) => ConstraintSet::from_bool(constraints, true), + } + }, + ) }) } @@ -680,22 +696,24 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { } } - pub(super) fn has_disjoint_type_from( + pub(super) fn has_disjoint_type_from<'c>( &self, db: &'db dyn Db, other: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - ) -> ConstraintSet<'db> { + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match &self.kind { // TODO: implement disjointness for property/method members as well as attribute members ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } ProtocolMemberKind::Other(ty) => ty.is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -705,15 +723,17 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { /// Return `true` if `other` contains an attribute/method/property that satisfies /// the part of the interface defined by this protocol member. - pub(super) fn is_satisfied_by( + #[expect(clippy::too_many_arguments)] + pub(super) fn is_satisfied_by<'c>( &self, db: &'db dyn Db, other: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match &self.kind { ProtocolMemberKind::Method(method) => { // `__call__` members must be special cased for several reasons: @@ -742,7 +762,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { ) .place else { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); }; attribute_type }; @@ -759,29 +779,35 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { // With the new solver, we should be to replace all of this with an additional // constraint that enforces what `Self` can specialize to. let fallback_other = other.literal_fallback_instance(db).unwrap_or(other); - attribute_type - .try_upcast_to_callable(db) - .when_some_and(|callables| { + attribute_type.try_upcast_to_callable(db).when_some_and( + db, + constraints, + |callables| { callables .map(|callable| callable.apply_self(db, fallback_other)) .has_relation_to_impl( db, protocol_bind_self(db, *method, Some(fallback_other)), + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - }) + }, + ) } // TODO: consider the types of the attribute on `other` for property members - ProtocolMemberKind::Property(_) => ConstraintSet::from(matches!( - other.member(db, self.name).place, - Place::Defined(DefinedPlace { - definedness: Definedness::AlwaysDefined, - .. - }) - )), + ProtocolMemberKind::Property(_) => ConstraintSet::from_bool( + constraints, + matches!( + other.member(db, self.name).place, + Place::Defined(DefinedPlace { + definedness: Definedness::AlwaysDefined, + .. + }) + ), + ), ProtocolMemberKind::Other(member_type) => { let Place::Defined(DefinedPlace { ty: attribute_type, @@ -789,21 +815,23 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { .. }) = other.member(db, self.name).place else { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); }; member_type .has_relation_to_impl( db, attribute_type, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { attribute_type.has_relation_to_impl( db, *member_type, + constraints, inferable, relation, relation_visitor, diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 1fb575bf3c540..da9f7a45fc5ef 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -4,7 +4,9 @@ use rustc_hash::FxHashSet; use crate::place::{DefinedPlace, Place}; use crate::types::builder::RecursivelyDefined; -use crate::types::constraints::{IteratorConstraintsExtension, OptionConstraintsExtension}; +use crate::types::constraints::{ + ConstraintSetBuilder, IteratorConstraintsExtension, OptionConstraintsExtension, +}; use crate::types::enums::is_single_member_enum; use crate::types::{ CallableType, ClassBase, ClassType, CycleDetector, DynamicType, KnownClass, KnownInstanceType, @@ -18,7 +20,7 @@ use crate::{ /// A non-exhaustive enumeration of relations that can exist between types. #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub(crate) enum TypeRelation<'db> { +pub(crate) enum TypeRelation { /// The "subtyping" relation. /// /// A [fully static] type `B` is a subtype of a fully static type `A` if and only if @@ -189,7 +191,7 @@ pub(crate) enum TypeRelation<'db> { /// subtype check will be vacuously true, even if you're comparing two concrete types that /// are not actually subtypes of each other. (That is, `implies_subtype_of(false, int, str)` /// will return true!) - SubtypingAssuming(ConstraintSet<'db>), + SubtypingAssuming, /// A placeholder for the new assignability relation that uses constraint sets to encode /// relationships with a typevar. This will eventually replace `Assignability`, but allows us @@ -197,7 +199,7 @@ pub(crate) enum TypeRelation<'db> { ConstraintSetAssignability, } -impl TypeRelation<'_> { +impl TypeRelation { pub(crate) const fn is_assignability(self) -> bool { matches!(self, TypeRelation::Assignability) } @@ -215,7 +217,7 @@ impl TypeRelation<'_> { TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability | TypeRelation::Redundancy { .. } => true, - TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => { + TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => { ty.subtyping_is_always_reflexive() } } @@ -228,35 +230,41 @@ impl<'db> Type<'db> { /// /// See [`TypeRelation::Subtyping`] for more details. pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { - self.when_subtype_of(db, target, InferableTypeVars::None) + let constraints = ConstraintSetBuilder::new(); + self.when_subtype_of(db, target, &constraints, InferableTypeVars::None) .is_always_satisfied(db) } - pub(super) fn when_subtype_of( + pub(super) fn when_subtype_of<'c>( self, db: &'db dyn Db, target: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { - self.has_relation_to(db, target, inferable, TypeRelation::Subtyping) + ) -> ConstraintSet<'db, 'c> { + self.has_relation_to(db, target, constraints, inferable, TypeRelation::Subtyping) } /// Return the constraints under which this type is a subtype of type `target`, assuming that /// all of the restrictions in `constraints` hold. /// /// See [`TypeRelation::SubtypingAssuming`] for more details. - pub(super) fn when_subtype_of_assuming( + pub(super) fn when_subtype_of_assuming<'c>( self, db: &'db dyn Db, target: Type<'db>, - assuming: ConstraintSet<'db>, + assuming: ConstraintSet<'db, 'c>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { - self.has_relation_to( + ) -> ConstraintSet<'db, 'c> { + self.has_relation_to_impl( db, target, + constraints, inferable, - TypeRelation::SubtypingAssuming(assuming), + TypeRelation::SubtypingAssuming, + &HasRelationToVisitor::with_given(constraints, assuming), + &IsDisjointVisitor::default(constraints), ) } @@ -264,7 +272,8 @@ impl<'db> Type<'db> { /// /// See `TypeRelation::Assignability` for more details. pub fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { - self.when_assignable_to(db, target, InferableTypeVars::None) + let constraints = ConstraintSetBuilder::new(); + self.when_assignable_to(db, target, &constraints, InferableTypeVars::None) .is_always_satisfied(db) } @@ -274,28 +283,38 @@ impl<'db> Type<'db> { /// a constraint set and lets `satisfied_by_all_typevars` perform existential vs universal /// reasoning depending on inferable typevars. pub fn is_constraint_set_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { - self.when_constraint_set_assignable_to(db, target, InferableTypeVars::None) + let constraints = ConstraintSetBuilder::new(); + self.when_constraint_set_assignable_to(db, target, &constraints, InferableTypeVars::None) .is_always_satisfied(db) } - pub(super) fn when_assignable_to( + pub(super) fn when_assignable_to<'c>( self, db: &'db dyn Db, target: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { - self.has_relation_to(db, target, inferable, TypeRelation::Assignability) + ) -> ConstraintSet<'db, 'c> { + self.has_relation_to( + db, + target, + constraints, + inferable, + TypeRelation::Assignability, + ) } - pub(super) fn when_constraint_set_assignable_to( + pub(super) fn when_constraint_set_assignable_to<'c>( self, db: &'db dyn Db, target: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { + ) -> ConstraintSet<'db, 'c> { self.has_relation_to( db, target, + constraints, inferable, TypeRelation::ConstraintSetAssignability, ) @@ -311,10 +330,12 @@ impl<'db> Type<'db> { self_ty: Type<'db>, other: Type<'db>, ) -> bool { + let constraints = ConstraintSetBuilder::new(); self_ty .has_relation_to( db, other, + &constraints, InferableTypeVars::None, TypeRelation::Redundancy { pure: false }, ) @@ -328,47 +349,52 @@ impl<'db> Type<'db> { is_redundant_with_impl(db, self, other) } - pub(super) fn has_relation_to( + pub(super) fn has_relation_to<'c>( self, db: &'db dyn Db, target: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + ) -> ConstraintSet<'db, 'c> { self.has_relation_to_impl( db, target, + constraints, inferable, relation, - &HasRelationToVisitor::default(), - &IsDisjointVisitor::default(), + &HasRelationToVisitor::default(constraints), + &IsDisjointVisitor::default(constraints), ) } - pub(super) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(super) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, target: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { // Subtyping implies assignability, so if subtyping is reflexive and the two types are // equal, it is both a subtype and assignable. Assignability is always reflexive. // // Note that we could do a full equivalence check here, but that would be both expensive // and unnecessary. This early return is only an optimisation. if relation.can_safely_assume_reflexivity(self) && self == target { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } // Handle constraint implication first. If either `self` or `target` is a typevar, check // the constraint set to see if the corresponding constraint is satisfied. - if let TypeRelation::SubtypingAssuming(constraints) = relation + if relation == TypeRelation::SubtypingAssuming && (self.is_type_var() || target.is_type_var()) { - return constraints.implies_subtype_of(db, self, target); + let given = relation_visitor.extra; + return given.implies_subtype_of(db, constraints, self, target); } // Handle the new constraint-set-based assignability relation next. Comparisons with a @@ -380,29 +406,41 @@ impl<'db> Type<'db> { // only has to hold when the typevar has a valid specialization (i.e., one that // satisfies the upper bound/constraints). if let Type::TypeVar(bound_typevar) = self { - return ConstraintSet::constrain_typevar(db, bound_typevar, Type::Never, target); + return ConstraintSet::constrain_typevar( + db, + constraints, + bound_typevar, + Type::Never, + target, + ); } else if let Type::TypeVar(bound_typevar) = target { - return ConstraintSet::constrain_typevar(db, bound_typevar, self, Type::object()); + return ConstraintSet::constrain_typevar( + db, + constraints, + bound_typevar, + self, + Type::object(), + ); } } match (self, target) { // Everything is a subtype of `object`. (_, Type::NominalInstance(instance)) if instance.is_object() => { - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } (_, Type::ProtocolInstance(target)) if target.is_equivalent_to_object(db) => { - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } // `Never` is the bottom type, the empty set. // It is a subtype of all other types. - (Type::Never, _) => ConstraintSet::from(true), + (Type::Never, _) => ConstraintSet::from_bool(constraints, true), (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) if self_typevar.is_same_typevar_as(db, other_typevar) => { - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } // In some specific situations, `Any`/`Unknown`/`@Todo` can be simplified out of unions and intersections, @@ -410,7 +448,7 @@ impl<'db> Type<'db> { // "too many cycle iterations" panics). (Type::Dynamic(DynamicType::Divergent(_)), _) | (_, Type::Dynamic(DynamicType::Divergent(_))) => { - ConstraintSet::from(relation.is_assignability()) + ConstraintSet::from_bool(constraints, relation.is_assignability()) } (Type::TypeAlias(self_alias), _) => { @@ -418,6 +456,7 @@ impl<'db> Type<'db> { self_alias.value_type(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -431,6 +470,7 @@ impl<'db> Type<'db> { self.has_relation_to_impl( db, target_alias.value_type(db), + constraints, inferable, relation, relation_visitor, @@ -445,16 +485,19 @@ impl<'db> Type<'db> { (Type::KnownInstance(KnownInstanceType::Field(field)), right) if relation.is_assignability() => { - field.default_type(db).when_none_or(|default_type| { - default_type.has_relation_to_impl( - db, - right, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + field + .default_type(db) + .when_none_or(db, constraints, |default_type| { + default_type.has_relation_to_impl( + db, + right, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) } // Dynamic is only a subtype of `object` and only a supertype of `Never`; both were @@ -470,31 +513,39 @@ impl<'db> Type<'db> { !matches!(dynamic, DynamicType::Divergent(_)), "DynamicType::Divergent should have been handled in an earlier branch" ); - ConstraintSet::from(match relation { - TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false, + ConstraintSet::from_bool( + constraints, + match relation { + TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => false, + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { + true + } + TypeRelation::Redundancy { .. } => match target { + Type::Dynamic(_) => true, + Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic), + _ => false, + }, + }, + ) + } + (_, Type::Dynamic(_)) => ConstraintSet::from_bool( + constraints, + match relation { + TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => false, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true, - TypeRelation::Redundancy { .. } => match target { + TypeRelation::Redundancy { .. } => match self { Type::Dynamic(_) => true, - Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic), + Type::Intersection(intersection) => { + // If a `Divergent` type is involved, it must not be eliminated. + intersection + .positive(db) + .iter() + .any(Type::is_non_divergent_dynamic) + } _ => false, }, - }) - } - (_, Type::Dynamic(_)) => ConstraintSet::from(match relation { - TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false, - TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true, - TypeRelation::Redundancy { .. } => match self { - Type::Dynamic(_) => true, - Type::Intersection(intersection) => { - // If a `Divergent` type is involved, it must not be eliminated. - intersection - .positive(db) - .iter() - .any(Type::is_non_divergent_dynamic) - } - _ => false, }, - }), + ), // In general, a TypeVar `T` is not redundant with a type `S` unless one of the two conditions is satisfied: // 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`. @@ -507,7 +558,7 @@ impl<'db> Type<'db> { if relation.can_safely_assume_reflexivity(self) && union.elements(db).contains(&self) => { - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } // A similar rule applies in reverse to intersection types. @@ -515,7 +566,7 @@ impl<'db> Type<'db> { if relation.can_safely_assume_reflexivity(target) && intersection.positive(db).contains(&target) => { - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } (Type::Intersection(intersection), _) if relation.is_assignability() @@ -524,13 +575,13 @@ impl<'db> Type<'db> { // If the intersection contains `Any`/`Unknown`/`@Todo`, it is assignable to any type. // `Any` could materialize to `Never`, `Never & T & ~S` simplifies to `Never` for any // `T` and any `S`, and `Never` is a subtype of all types. - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } (Type::Intersection(intersection), _) if relation.can_safely_assume_reflexivity(target) && intersection.negative(db).contains(&target) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } // `type[T]` is a subtype of the class object `A` if every instance of `T` is a subtype of an instance @@ -539,10 +590,11 @@ impl<'db> Type<'db> { if !subclass_of .into_type_var() .zip(target.to_instance(db)) - .when_some_and(|(this_instance, other_instance)| { + .when_some_and(db, constraints, |(this_instance, other_instance)| { Type::TypeVar(this_instance).has_relation_to_impl( db, other_instance, + constraints, inferable, relation, relation_visitor, @@ -555,10 +607,11 @@ impl<'db> Type<'db> { subclass_of .into_type_var() .zip(target.to_instance(db)) - .when_some_and(|(this_instance, other_instance)| { + .when_some_and(db, constraints, |(this_instance, other_instance)| { Type::TypeVar(this_instance).has_relation_to_impl( db, other_instance, + constraints, inferable, relation, relation_visitor, @@ -571,10 +624,11 @@ impl<'db> Type<'db> { if !subclass_of .into_type_var() .zip(self.to_instance(db)) - .when_some_and(|(other_instance, this_instance)| { + .when_some_and(db, constraints, |(other_instance, this_instance)| { this_instance.has_relation_to_impl( db, Type::TypeVar(other_instance), + constraints, inferable, relation, relation_visitor, @@ -587,10 +641,11 @@ impl<'db> Type<'db> { subclass_of .into_type_var() .zip(self.to_instance(db)) - .when_some_and(|(other_instance, this_instance)| { + .when_some_and(db, constraints, |(other_instance, this_instance)| { this_instance.has_relation_to_impl( db, Type::TypeVar(other_instance), + constraints, inferable, relation, relation_visitor, @@ -612,22 +667,28 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.elements(db).iter().when_all(db, |constraint| { - constraint.has_relation_to_impl( - db, - target, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { + typevar_constraints.elements(db).iter().when_all( + db, + constraints, + |constraint| { + constraint.has_relation_to_impl( + db, + target, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }, + ) } } } @@ -640,39 +701,46 @@ impl<'db> Type<'db> { && !bound_typevar .typevar(db) .constraints(db) - .when_some_and(|constraints| { - constraints.iter().when_all(db, |constraint| { + .when_some_and(db, constraints, |typevar_constraints| { + typevar_constraints + .iter() + .when_all(db, constraints, |constraint| { + self.has_relation_to_impl( + db, + *constraint, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + }) + .is_never_satisfied(db) => + { + // TODO: The repetition here isn't great, but we really need the fallthrough logic, + // where this arm only engages if it returns true (or in the world of constraints, + // not false). Once we're using real constraint sets instead of bool, we should be + // able to simplify the typevar logic. + bound_typevar.typevar(db).constraints(db).when_some_and( + db, + constraints, + |typevar_constraints| { + typevar_constraints + .iter() + .when_all(db, constraints, |constraint| { self.has_relation_to_impl( db, *constraint, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) }) - }) - .is_never_satisfied(db) => - { - // TODO: The repetition here isn't great, but we really need the fallthrough logic, - // where this arm only engages if it returns true (or in the world of constraints, - // not false). Once we're using real constraint sets instead of bool, we should be - // able to simplify the typevar logic. - bound_typevar - .typevar(db) - .constraints(db) - .when_some_and(|constraints| { - constraints.iter().when_all(db, |constraint| { - self.has_relation_to_impl( - db, - *constraint, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - }) + }, + ) } (Type::TypeVar(bound_typevar), _) if bound_typevar.is_inferable(db, inferable) => { @@ -681,7 +749,7 @@ impl<'db> Type<'db> { // TODO: record the unification constraints - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } // Fast path for various types that we know `object` is never a subtype of @@ -693,21 +761,21 @@ impl<'db> Type<'db> { | Type::SubclassOf(_) | Type::Callable(_) | Type::ProtocolInstance(_), - ) if source.is_object() => ConstraintSet::from(false), + ) if source.is_object() => ConstraintSet::from_bool(constraints, false), // Fast path: `object` is not a subtype of any non-inferable type variable, since the // type variable could be specialized to a type smaller than `object`. (Type::NominalInstance(source), Type::TypeVar(typevar)) if source.is_object() && !typevar.is_inferable(db, inferable) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } // `Never` is the bottom type, the empty set. - (_, Type::Never) => ConstraintSet::from(false), + (_, Type::Never) => ConstraintSet::from_bool(constraints, false), (Type::NewTypeInstance(self_newtype), Type::NewTypeInstance(target_newtype)) => { - self_newtype.has_relation_to_impl(db, target_newtype) + self_newtype.has_relation_to_impl(db, target_newtype, constraints) } // In the special cases of `NewType`s of `float` or `complex`, the concrete base type // can be a union (`int | float` or `int | float | complex`). For that reason, @@ -738,10 +806,11 @@ impl<'db> Type<'db> { union .elements(db) .iter() - .when_any(db, |&elem_ty| { + .when_any(db, constraints, |&elem_ty| { self.has_relation_to_impl( db, elem_ty, + constraints, inferable, relation, relation_visitor, @@ -751,44 +820,57 @@ impl<'db> Type<'db> { // Failing that, if the concrete base type is a union, try delegating to that. // Otherwise, this would be equivalent to what we just checked, and we // shouldn't waste time checking it twice. - .or(db, || { + .or(db, constraints, || { let concrete_base = self_newtype.concrete_base_type(db); if matches!(concrete_base, Type::Union(_)) { concrete_base.has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) } else { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } }) } - (Type::Union(union), _) => union.elements(db).iter().when_all(db, |&elem_ty| { - elem_ty.has_relation_to_impl( - db, - target, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), + (Type::Union(union), _) => { + union + .elements(db) + .iter() + .when_all(db, constraints, |&elem_ty| { + elem_ty.has_relation_to_impl( + db, + target, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } - (_, Type::Union(union)) => union.elements(db).iter().when_any(db, |&elem_ty| { - self.has_relation_to_impl( - db, - elem_ty, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), + (_, Type::Union(union)) => { + union + .elements(db) + .iter() + .when_any(db, constraints, |&elem_ty| { + self.has_relation_to_impl( + db, + elem_ty, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } // If both sides are intersections we need to handle the right side first // (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B, @@ -796,17 +878,18 @@ impl<'db> Type<'db> { (_, Type::Intersection(intersection)) => intersection .positive(db) .iter() - .when_all(db, |&pos_ty| { + .when_all(db, constraints, |&pos_ty| { self.has_relation_to_impl( db, pos_ty, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) }) - .and(db, || { + .and(db, constraints, || { // For subtyping, we would want to check whether the *top materialization* of `self` // is disjoint from the *top materialization* of `neg_ty`. As an optimization, however, // we can avoid this explicit transformation here, since our `Type::is_disjoint_from` @@ -823,29 +906,33 @@ impl<'db> Type<'db> { let self_ty = match relation { TypeRelation::Subtyping | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming(_) => self, + | TypeRelation::SubtypingAssuming => self, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { self.bottom_materialization(db) } }; - intersection.negative(db).iter().when_all(db, |&neg_ty| { - let neg_ty = match relation { - TypeRelation::Subtyping - | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming(_) => neg_ty, - TypeRelation::Assignability - | TypeRelation::ConstraintSetAssignability => { - neg_ty.bottom_materialization(db) - } - }; - self_ty.is_disjoint_from_impl( - db, - neg_ty, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) + intersection + .negative(db) + .iter() + .when_all(db, constraints, |&neg_ty| { + let neg_ty = match relation { + TypeRelation::Subtyping + | TypeRelation::Redundancy { .. } + | TypeRelation::SubtypingAssuming => neg_ty, + TypeRelation::Assignability + | TypeRelation::ConstraintSetAssignability => { + neg_ty.bottom_materialization(db) + } + }; + self_ty.is_disjoint_from_impl( + db, + neg_ty, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ) + }) }), (Type::Intersection(intersection), _) => { @@ -855,10 +942,11 @@ impl<'db> Type<'db> { // `object & ~str`). intersection .positive_elements_or_object(db) - .when_any(db, |elem_ty| { + .when_any(db, constraints, |elem_ty| { elem_ty.has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -873,7 +961,7 @@ impl<'db> Type<'db> { // bound. This is true even if the bound is a final class, since the typevar can still // be specialized to `Never`.) (_, Type::TypeVar(bound_typevar)) if !bound_typevar.is_inferable(db, inferable) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } (_, Type::TypeVar(typevar)) @@ -884,6 +972,7 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, bound, + constraints, inferable, relation, relation_visitor, @@ -894,26 +983,30 @@ impl<'db> Type<'db> { { // TODO: record the unification constraints - typevar.typevar(db).upper_bound(db).when_none_or(|bound| { - self.has_relation_to_impl( - db, - bound, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + typevar + .typevar(db) + .upper_bound(db) + .when_none_or(db, constraints, |bound| { + self.has_relation_to_impl( + db, + bound, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) } // TODO: Infer specializations here (_, Type::TypeVar(bound_typevar)) if bound_typevar.is_inferable(db, inferable) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } (Type::TypeVar(bound_typevar), _) => { // All inferable cases should have been handled above assert!(!bound_typevar.is_inferable(db, inferable)); - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } // All other `NewType` assignments fall back to the concrete base type. @@ -924,6 +1017,7 @@ impl<'db> Type<'db> { self_newtype.concrete_base_type(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -933,14 +1027,19 @@ impl<'db> Type<'db> { // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. - (left, Type::AlwaysFalsy) => ConstraintSet::from(left.bool(db).is_always_false()), - (left, Type::AlwaysTruthy) => ConstraintSet::from(left.bool(db).is_always_true()), + (left, Type::AlwaysFalsy) => { + ConstraintSet::from_bool(constraints, left.bool(db).is_always_false()) + } + (left, Type::AlwaysTruthy) => { + ConstraintSet::from_bool(constraints, left.bool(db).is_always_true()) + } // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { relation_visitor.visit((self, target, relation), || { Type::object().has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -958,6 +1057,7 @@ impl<'db> Type<'db> { self_function.has_relation_to_impl( db, target_function, + constraints, inferable, relation, relation_visitor, @@ -968,6 +1068,7 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, target_method, + constraints, inferable, relation, relation_visitor, @@ -977,6 +1078,7 @@ impl<'db> Type<'db> { self_method.has_relation_to_impl( db, target_method, + constraints, inferable, relation, relation_visitor, @@ -988,7 +1090,7 @@ impl<'db> Type<'db> { (Type::LiteralValue(this), Type::LiteralValue(target)) if this.is_string() && target.is_literal_string() => { - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } // For union simplification, we want to preserve the unpromotable form of a literal value, @@ -996,11 +1098,14 @@ impl<'db> Type<'db> { (Type::LiteralValue(this), Type::LiteralValue(target)) if matches!(relation, TypeRelation::Redundancy { pure: false }) => { - ConstraintSet::from(this.kind() == target.kind() && this.is_promotable()) + ConstraintSet::from_bool( + constraints, + this.kind() == target.kind() && this.is_promotable(), + ) } (Type::LiteralValue(this), Type::LiteralValue(target)) => { - ConstraintSet::from(this.kind() == target.kind()) + ConstraintSet::from_bool(constraints, this.kind() == target.kind()) } // No literal type is a subtype of any other literal type, unless they are the same @@ -1016,13 +1121,14 @@ impl<'db> Type<'db> { | Type::ClassLiteral(_) | Type::FunctionLiteral(_) | Type::ModuleLiteral(_), - ) => ConstraintSet::from(false), + ) => ConstraintSet::from_bool(constraints, false), (Type::Callable(self_callable), Type::Callable(other_callable)) => relation_visitor .visit((self, target, relation), || { self_callable.has_relation_to_impl( db, other_callable, + constraints, inferable, relation, relation_visitor, @@ -1032,16 +1138,18 @@ impl<'db> Type<'db> { (_, Type::Callable(other_callable)) => { relation_visitor.visit((self, target, relation), || { - self.try_upcast_to_callable(db).when_some_and(|callables| { - callables.has_relation_to_impl( - db, - other_callable, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + self.try_upcast_to_callable(db) + .when_some_and(db, constraints, |callables| { + callables.has_relation_to_impl( + db, + other_callable, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) }) } @@ -1058,6 +1166,7 @@ impl<'db> Type<'db> { KnownClass::Type.to_instance(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1070,6 +1179,7 @@ impl<'db> Type<'db> { self.satisfies_protocol( db, protocol, + constraints, inferable, relation, relation_visitor, @@ -1079,13 +1189,14 @@ impl<'db> Type<'db> { } // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`. - (Type::ProtocolInstance(_), _) => ConstraintSet::from(false), + (Type::ProtocolInstance(_), _) => ConstraintSet::from_bool(constraints, false), (Type::TypedDict(self_typeddict), Type::TypedDict(other_typeddict)) => relation_visitor .visit((self, target, relation), || { self_typeddict.has_relation_to_impl( db, other_typeddict, + constraints, inferable, relation, relation_visitor, @@ -1103,6 +1214,7 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1111,7 +1223,7 @@ impl<'db> Type<'db> { }), // A non-`TypedDict` cannot subtype a `TypedDict` - (_, Type::TypedDict(_)) => ConstraintSet::from(false), + (_, Type::TypedDict(_)) => ConstraintSet::from_bool(constraints, false), // A string literal `Literal["abc"]` is assignable to `str` *and* to // `Sequence[Literal["a", "b", "c"]]` because strings are sequences of their characters. @@ -1122,7 +1234,7 @@ impl<'db> Type<'db> { let other_class = instance.class(db); if other_class.is_known(db, KnownClass::Str) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db) @@ -1132,7 +1244,7 @@ impl<'db> Type<'db> { .map(|class| class.class_literal(db)) .contains(&other_class.class_literal(db)) { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } let chars: FxHashSet = value.value(db).chars().collect(); @@ -1154,10 +1266,11 @@ impl<'db> Type<'db> { KnownClass::Sequence .to_specialized_class_type(db, &[spec]) - .when_some_and(|sequence| { + .when_some_and(db, constraints, |sequence| { sequence.has_relation_to_impl( db, other_class, + constraints, inferable, relation, relation_visitor, @@ -1166,7 +1279,9 @@ impl<'db> Type<'db> { }) } - (Type::LiteralValue(literal), _) if literal.is_string() => ConstraintSet::from(false), + (Type::LiteralValue(literal), _) if literal.is_string() => { + ConstraintSet::from_bool(constraints, false) + } // A bytes literal `Literal[b"abc"]` is assignable to `bytes` *and* to // `Sequence[Literal[97, 98, 99]]` because bytes are sequences of integers. @@ -1177,7 +1292,7 @@ impl<'db> Type<'db> { let other_class = instance.class(db); if other_class.is_known(db, KnownClass::Bytes) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db) @@ -1187,7 +1302,7 @@ impl<'db> Type<'db> { .map(|class| class.class_literal(db)) .contains(&other_class.class_literal(db)) { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } let ints: FxHashSet = value @@ -1208,10 +1323,11 @@ impl<'db> Type<'db> { KnownClass::Sequence .to_specialized_class_type(db, &[spec]) - .when_some_and(|sequence| { + .when_some_and(db, constraints, |sequence| { sequence.has_relation_to_impl( db, other_class, + constraints, inferable, relation, relation_visitor, @@ -1220,30 +1336,33 @@ impl<'db> Type<'db> { }) } - (Type::LiteralValue(literal), _) if literal.is_bytes() => ConstraintSet::from(false), + (Type::LiteralValue(literal), _) if literal.is_bytes() => { + ConstraintSet::from_bool(constraints, false) + } // An instance is a subtype of an enum literal, if it is an instance of the enum class // and the enum has only one member. (Type::NominalInstance(_), Type::LiteralValue(literal)) if literal.is_enum() => { let target_enum_literal = literal.as_enum().unwrap(); if target_enum_literal.enum_class_instance(db) != self { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } - ConstraintSet::from(is_single_member_enum( - db, - target_enum_literal.enum_class(db), - )) + ConstraintSet::from_bool( + constraints, + is_single_member_enum(db, target_enum_literal.enum_class(db)), + ) } // Except for the special `BytesLiteral`, `LiteralString`, and string literal cases above, // most `Literal` types delegate to their instance fallbacks // unless `self` is exactly equivalent to `target` (handled above) (Type::ModuleLiteral(_) | Type::LiteralValue(_) | Type::FunctionLiteral(_), _) => { - (self.literal_fallback_instance(db)).when_some_and(|instance| { + (self.literal_fallback_instance(db)).when_some_and(db, constraints, |instance| { instance.has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1257,6 +1376,7 @@ impl<'db> Type<'db> { KnownClass::MethodType.to_instance(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1267,6 +1387,7 @@ impl<'db> Type<'db> { method.class().to_instance(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1278,6 +1399,7 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1286,7 +1408,7 @@ impl<'db> Type<'db> { (Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => { // TODO: Implement subtyping using an equivalent `Callable` type. - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } // `TypeIs` is invariant. @@ -1295,15 +1417,17 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, right.return_type(db), + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { right.return_type(db).has_relation_to_impl( db, left.return_type(db), + constraints, inferable, relation, relation_visitor, @@ -1316,6 +1440,7 @@ impl<'db> Type<'db> { left.return_type(db).has_relation_to_impl( db, right.return_type(db), + constraints, inferable, relation, relation_visitor, @@ -1328,6 +1453,7 @@ impl<'db> Type<'db> { KnownClass::Bool.to_instance(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1342,6 +1468,7 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1349,14 +1476,19 @@ impl<'db> Type<'db> { ) } - (Type::Callable(_), _) => ConstraintSet::from(false), + (Type::Callable(_), _) => ConstraintSet::from_bool(constraints, false), - (Type::BoundSuper(left), Type::BoundSuper(right)) => { - left.is_equivalent_to_impl(db, right, relation_visitor, disjointness_visitor) - } + (Type::BoundSuper(left), Type::BoundSuper(right)) => left.is_equivalent_to_impl( + db, + right, + constraints, + relation_visitor, + disjointness_visitor, + ), (Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1366,7 +1498,7 @@ impl<'db> Type<'db> { (Type::SubclassOf(subclass_of), _) | (_, Type::SubclassOf(subclass_of)) if subclass_of.is_type_var() => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } // `Literal[]` is a subtype of `type[B]` if `C` is a subclass of `B`, @@ -1378,13 +1510,16 @@ impl<'db> Type<'db> { class.default_specialization(db).has_relation_to_impl( db, subclass_of_class, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) }) - .unwrap_or_else(|| ConstraintSet::from(relation.is_assignability())), + .unwrap_or_else(|| { + ConstraintSet::from_bool(constraints, relation.is_assignability()) + }), // Similarly, `` is assignable to `` (a generic-alias type) // if the default specialization of `C` is assignable to `C[...]`. This scenario occurs @@ -1394,6 +1529,7 @@ impl<'db> Type<'db> { class.default_specialization(db).has_relation_to_impl( db, ClassType::Generic(target_alias), + constraints, inferable, relation, relation_visitor, @@ -1406,6 +1542,7 @@ impl<'db> Type<'db> { ClassType::Generic(self_alias).has_relation_to_impl( db, ClassType::Generic(target_alias), + constraints, inferable, relation, relation_visitor, @@ -1420,19 +1557,23 @@ impl<'db> Type<'db> { ClassType::Generic(alias).has_relation_to_impl( db, subclass_of_class, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) }) - .unwrap_or_else(|| ConstraintSet::from(relation.is_assignability())), + .unwrap_or_else(|| { + ConstraintSet::from_bool(constraints, relation.is_assignability()) + }), // This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`? (Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => { self_subclass_ty.has_relation_to_impl( db, target_subclass_ty, + constraints, inferable, relation, relation_visitor, @@ -1447,6 +1588,7 @@ impl<'db> Type<'db> { class.metaclass_instance_type(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1458,6 +1600,7 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1471,22 +1614,28 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, other, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - .or(db, || { - ConstraintSet::from(relation.is_assignability()).and(db, || { - other.has_relation_to_impl( - db, - KnownClass::Type.to_instance(db), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + .or(db, constraints, || { + ConstraintSet::from_bool(constraints, relation.is_assignability()).and( + db, + constraints, + || { + other.has_relation_to_impl( + db, + KnownClass::Type.to_instance(db), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }, + ) }) } @@ -1497,6 +1646,7 @@ impl<'db> Type<'db> { other.has_relation_to_impl( db, KnownClass::Type.to_instance(db), + constraints, inferable, relation, relation_visitor, @@ -1519,6 +1669,7 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1531,6 +1682,7 @@ impl<'db> Type<'db> { (Type::SpecialForm(left), right) => left.instance_fallback(db).has_relation_to_impl( db, right, + constraints, inferable, relation, relation_visitor, @@ -1540,6 +1692,7 @@ impl<'db> Type<'db> { (Type::KnownInstance(left), right) => left.instance_fallback(db).has_relation_to_impl( db, right, + constraints, inferable, relation, relation_visitor, @@ -1553,6 +1706,7 @@ impl<'db> Type<'db> { self_instance.has_relation_to_impl( db, target_instance, + constraints, inferable, relation, relation_visitor, @@ -1565,6 +1719,7 @@ impl<'db> Type<'db> { KnownClass::Property.to_instance(db).has_relation_to_impl( db, target, + constraints, inferable, relation, relation_visitor, @@ -1574,6 +1729,7 @@ impl<'db> Type<'db> { (_, Type::PropertyInstance(_)) => self.has_relation_to_impl( db, KnownClass::Property.to_instance(db), + constraints, inferable, relation, relation_visitor, @@ -1582,7 +1738,7 @@ impl<'db> Type<'db> { // Other than the special cases enumerated above, nominal-instance types are never // subtypes of any other variants - (Type::NominalInstance(_), _) => ConstraintSet::from(false), + (Type::NominalInstance(_), _) => ConstraintSet::from_bool(constraints, false), } } @@ -1599,38 +1755,50 @@ impl<'db> Type<'db> { /// /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { - self.when_equivalent_to(db, other).is_always_satisfied(db) + let constraints = ConstraintSetBuilder::new(); + self.when_equivalent_to(db, other, &constraints) + .is_always_satisfied(db) } - pub(crate) fn when_equivalent_to( + pub(crate) fn when_equivalent_to<'c>( self, db: &'db dyn Db, other: Type<'db>, - ) -> ConstraintSet<'db> { - let relation_visitor = HasRelationToVisitor::default(); - let disjointness_visitor = IsDisjointVisitor::default(); - self.when_equivalent_to_impl(db, other, &relation_visitor, &disjointness_visitor) + constraints: &'c ConstraintSetBuilder<'db>, + ) -> ConstraintSet<'db, 'c> { + let relation_visitor = HasRelationToVisitor::default(constraints); + let disjointness_visitor = IsDisjointVisitor::default(constraints); + self.when_equivalent_to_impl( + db, + other, + constraints, + &relation_visitor, + &disjointness_visitor, + ) } - pub(crate) fn when_equivalent_to_impl( + pub(crate) fn when_equivalent_to_impl<'c>( self, db: &'db dyn Db, other: Type<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + constraints: &'c ConstraintSetBuilder<'db>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { self.has_relation_to_impl( db, other, + constraints, InferableTypeVars::None, TypeRelation::Redundancy { pure: true }, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { other.has_relation_to_impl( db, self, + constraints, InferableTypeVars::None, TypeRelation::Redundancy { pure: true }, relation_visitor, @@ -1655,62 +1823,73 @@ impl<'db> Type<'db> { /// This function aims to have no false positives, but might return wrong /// `false` answers in some cases. pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { - self.when_disjoint_from(db, other, InferableTypeVars::None) + let constraints = ConstraintSetBuilder::new(); + self.when_disjoint_from(db, other, &constraints, InferableTypeVars::None) .is_always_satisfied(db) } - pub(crate) fn when_disjoint_from( + pub(crate) fn when_disjoint_from<'c>( self, db: &'db dyn Db, other: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { + ) -> ConstraintSet<'db, 'c> { self.is_disjoint_from_impl( db, other, + constraints, inferable, - &IsDisjointVisitor::default(), - &HasRelationToVisitor::default(), + &IsDisjointVisitor::default(constraints), + &HasRelationToVisitor::default(constraints), ) } - pub(crate) fn is_disjoint_from_impl( + pub(crate) fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - ) -> ConstraintSet<'db> { - fn any_protocol_members_absent_or_disjoint<'db>( + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + fn any_protocol_members_absent_or_disjoint<'db, 'c>( db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, other: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - ) -> ConstraintSet<'db> { - protocol.interface(db).members(db).when_any(db, |member| { - other - .member(db, member.name()) - .place - .ignore_possibly_undefined() - .when_none_or(|attribute_type| { - member.has_disjoint_type_from( - db, - attribute_type, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) - }) + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + protocol + .interface(db) + .members(db) + .when_any(db, constraints, |member| { + other + .member(db, member.name()) + .place + .ignore_possibly_undefined() + .when_none_or(db, constraints, |attribute_type| { + member.has_disjoint_type_from( + db, + attribute_type, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ) + }) + }) } match (self, other) { - (Type::Never, _) | (_, Type::Never) => ConstraintSet::from(true), + (Type::Never, _) | (_, Type::Never) => ConstraintSet::from_bool(constraints, true), - (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => ConstraintSet::from(false), + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => { + ConstraintSet::from_bool(constraints, false) + } (Type::TypeAlias(alias), _) => { let self_alias_ty = alias.value_type(db); @@ -1718,6 +1897,7 @@ impl<'db> Type<'db> { self_alias_ty.is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1731,6 +1911,7 @@ impl<'db> Type<'db> { self.is_disjoint_from_impl( db, other_alias_ty, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1752,6 +1933,7 @@ impl<'db> Type<'db> { Type::TypeVar(type_var).is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1763,10 +1945,11 @@ impl<'db> Type<'db> { if !subclass_of .into_type_var() .zip(other.to_instance(db)) - .when_none_or(|(this_instance, other_instance)| { + .when_none_or(db, constraints, |(this_instance, other_instance)| { Type::TypeVar(this_instance).is_disjoint_from_impl( db, other_instance, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1778,10 +1961,11 @@ impl<'db> Type<'db> { subclass_of .into_type_var() .zip(other.to_instance(db)) - .when_none_or(|(this_instance, other_instance)| { + .when_none_or(db, constraints, |(this_instance, other_instance)| { Type::TypeVar(this_instance).is_disjoint_from_impl( db, other_instance, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1797,7 +1981,7 @@ impl<'db> Type<'db> { if !self_bound_typevar.is_inferable(db, inferable) && self_bound_typevar.is_same_typevar_as(db, other_bound_typevar) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } (tvar @ Type::TypeVar(bound_typevar), Type::Intersection(intersection)) @@ -1805,7 +1989,7 @@ impl<'db> Type<'db> { if !bound_typevar.is_inferable(db, inferable) && intersection.negative(db).contains(&tvar) => { - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } // An unbounded typevar is never disjoint from any other type, since it might be @@ -1816,37 +2000,46 @@ impl<'db> Type<'db> { if !bound_typevar.is_inferable(db, inferable) => { match bound_typevar.typevar(db).bound_or_constraints(db) { - None => ConstraintSet::from(false), + None => ConstraintSet::from_bool(constraints, false), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound .is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, ), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.elements(db).iter().when_all(db, |constraint| { - constraint.is_disjoint_from_impl( - db, - other, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) + Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { + typevar_constraints.elements(db).iter().when_all( + db, + constraints, + |constraint| { + constraint.is_disjoint_from_impl( + db, + other, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ) + }, + ) } } } // TODO: Infer specializations here - (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => ConstraintSet::from(false), + (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => { + ConstraintSet::from_bool(constraints, false) + } (Type::Union(union), other) | (other, Type::Union(union)) => { - union.elements(db).iter().when_all(db, |e| { + union.elements(db).iter().when_all(db, constraints, |e| { e.is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1862,25 +2055,30 @@ impl<'db> Type<'db> { self_intersection .positive(db) .iter() - .when_any(db, |p| { + .when_any(db, constraints, |p| { p.is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, ) }) - .or(db, || { - other_intersection.positive(db).iter().when_any(db, |p| { - p.is_disjoint_from_impl( - db, - self, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) + .or(db, constraints, || { + other_intersection + .positive(db) + .iter() + .when_any(db, constraints, |p| { + p.is_disjoint_from_impl( + db, + self, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ) + }) }) }) } @@ -1891,27 +2089,32 @@ impl<'db> Type<'db> { intersection .positive(db) .iter() - .when_any(db, |p| { + .when_any(db, constraints, |p| { p.is_disjoint_from_impl( db, non_intersection, + constraints, inferable, disjointness_visitor, relation_visitor, ) }) // A & B & Not[C] is disjoint from C - .or(db, || { - intersection.negative(db).iter().when_any(db, |&neg_ty| { - non_intersection.has_relation_to_impl( - db, - neg_ty, - inferable, - TypeRelation::Subtyping, - relation_visitor, - disjointness_visitor, - ) - }) + .or(db, constraints, || { + intersection + .negative(db) + .iter() + .when_any(db, constraints, |&neg_ty| { + non_intersection.has_relation_to_impl( + db, + neg_ty, + constraints, + inferable, + TypeRelation::Subtyping, + relation_visitor, + disjointness_visitor, + ) + }) }) }) } @@ -1921,11 +2124,11 @@ impl<'db> Type<'db> { || (this.is_string() && target.is_literal_string()) || (this.is_literal_string() && target.is_string()) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } (Type::LiteralValue(left), Type::LiteralValue(right)) => { - ConstraintSet::from(left.kind() != right.kind()) + ConstraintSet::from_bool(constraints, left.kind() != right.kind()) } // any single-valued type is disjoint from another single-valued type @@ -1948,7 +2151,7 @@ impl<'db> Type<'db> { | Type::ClassLiteral(..) | Type::SpecialForm(..) | Type::KnownInstance(..)), - ) => ConstraintSet::from(left != right), + ) => ConstraintSet::from_bool(constraints, left != right), ( Type::SubclassOf(_), @@ -1967,21 +2170,27 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..), Type::SubclassOf(_), - ) => ConstraintSet::from(true), + ) => ConstraintSet::from_bool(constraints, true), (Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => { // `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint. // Thus, they are only disjoint if `ty.bool() == AlwaysFalse`. - ConstraintSet::from(ty.bool(db).is_always_false()) + ConstraintSet::from_bool(constraints, ty.bool(db).is_always_false()) } (Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => { // Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`. - ConstraintSet::from(ty.bool(db).is_always_true()) + ConstraintSet::from_bool(constraints, ty.bool(db).is_always_true()) } (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => disjointness_visitor .visit((self, other), || { - left.is_disjoint_from_impl(db, right, inferable, disjointness_visitor) + left.is_disjoint_from_impl( + db, + right, + constraints, + inferable, + disjointness_visitor, + ) }), (Type::ProtocolInstance(protocol), Type::SpecialForm(special_form)) @@ -1991,6 +2200,7 @@ impl<'db> Type<'db> { db, protocol, special_form.instance_fallback(db), + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2005,6 +2215,7 @@ impl<'db> Type<'db> { db, protocol, known_instance.instance_fallback(db), + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2059,6 +2270,7 @@ impl<'db> Type<'db> { db, protocol, ty, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2077,6 +2289,7 @@ impl<'db> Type<'db> { db, protocol, nominal, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2087,34 +2300,43 @@ impl<'db> Type<'db> { (Type::ProtocolInstance(protocol), other) | (other, Type::ProtocolInstance(protocol)) => { disjointness_visitor.visit((self, other), || { - protocol.interface(db).members(db).when_any(db, |member| { - match other.member(db, member.name()).place { - Place::Defined(DefinedPlace { - ty: attribute_type, .. - }) => member.has_disjoint_type_from( - db, - attribute_type, - inferable, - disjointness_visitor, - relation_visitor, - ), - Place::Undefined => ConstraintSet::from(false), - } - }) + protocol + .interface(db) + .members(db) + .when_any(db, constraints, |member| { + match other.member(db, member.name()).place { + Place::Defined(DefinedPlace { + ty: attribute_type, .. + }) => member.has_disjoint_type_from( + db, + attribute_type, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ), + Place::Undefined => ConstraintSet::from_bool(constraints, false), + } + }) }) } (Type::SubclassOf(subclass_of_ty), _) | (_, Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_type_var() => { - ConstraintSet::from(true) + ConstraintSet::from_bool(constraints, true) } (Type::GenericAlias(left_alias), Type::GenericAlias(right_alias)) => { - ConstraintSet::from(left_alias.origin(db) != right_alias.origin(db)).or(db, || { + ConstraintSet::from_bool( + constraints, + left_alias.origin(db) != right_alias.origin(db), + ) + .or(db, constraints, || { left_alias.specialization(db).is_disjoint_from_impl( db, right_alias.specialization(db), + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2126,10 +2348,11 @@ impl<'db> Type<'db> { | (other @ Type::GenericAlias(_), Type::ClassLiteral(class_literal)) => class_literal .default_specialization(db) .into_generic_alias() - .when_none_or(|alias| { + .when_none_or(db, constraints, |alias| { other.is_disjoint_from_impl( db, Type::GenericAlias(alias), + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2139,9 +2362,14 @@ impl<'db> Type<'db> { (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { - SubclassOfInner::Dynamic(_) => ConstraintSet::from(false), - SubclassOfInner::Class(class_a) => ConstraintSet::from( - !class_a.could_exist_in_mro_of(db, ClassType::NonGeneric(class_b)), + SubclassOfInner::Dynamic(_) => ConstraintSet::from_bool(constraints, false), + SubclassOfInner::Class(class_a) => ConstraintSet::from_bool( + constraints, + !class_a.could_exist_in_mro_of( + db, + ClassType::NonGeneric(class_b), + constraints, + ), ), SubclassOfInner::TypeVar(_) => unreachable!(), } @@ -2150,16 +2378,21 @@ impl<'db> Type<'db> { (Type::SubclassOf(subclass_of_ty), Type::GenericAlias(alias_b)) | (Type::GenericAlias(alias_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { - SubclassOfInner::Dynamic(_) => ConstraintSet::from(false), - SubclassOfInner::Class(class_a) => ConstraintSet::from( - !class_a.could_exist_in_mro_of(db, ClassType::Generic(alias_b)), + SubclassOfInner::Dynamic(_) => ConstraintSet::from_bool(constraints, false), + SubclassOfInner::Class(class_a) => ConstraintSet::from_bool( + constraints, + !class_a.could_exist_in_mro_of( + db, + ClassType::Generic(alias_b), + constraints, + ), ), SubclassOfInner::TypeVar(_) => unreachable!(), } } (Type::SubclassOf(left), Type::SubclassOf(right)) => { - left.is_disjoint_from_impl(db, right, inferable, disjointness_visitor) + left.is_disjoint_from_impl(db, right, constraints, inferable, disjointness_visitor) } // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`, @@ -2170,6 +2403,7 @@ impl<'db> Type<'db> { KnownClass::Type.to_instance(db).is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2179,6 +2413,7 @@ impl<'db> Type<'db> { class.metaclass_instance_type(db).is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2189,42 +2424,49 @@ impl<'db> Type<'db> { (Type::SpecialForm(special_form), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => { - ConstraintSet::from(!special_form.is_instance_of(db, instance.class(db))) + ConstraintSet::from_bool( + constraints, + !special_form.is_instance_of(db, instance.class(db)), + ) } (Type::KnownInstance(known_instance), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { - ConstraintSet::from(!known_instance.is_instance_of(db, instance.class(db))) + ConstraintSet::from_bool( + constraints, + !known_instance.is_instance_of(db, instance.class(db)), + ) } (Type::LiteralValue(literal), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::LiteralValue(literal)) => { match literal.kind() { LiteralValueTypeKind::Int(_) => KnownClass::Int - .when_subclass_of(db, instance.class(db)) - .negate(db), + .when_subclass_of(db, instance.class(db), constraints) + .negate(db, constraints), LiteralValueTypeKind::Bool(_) => KnownClass::Bool - .when_subclass_of(db, instance.class(db)) - .negate(db), + .when_subclass_of(db, instance.class(db), constraints) + .negate(db, constraints), LiteralValueTypeKind::LiteralString | LiteralValueTypeKind::String(_) => { KnownClass::Str - .when_subclass_of(db, instance.class(db)) - .negate(db) + .when_subclass_of(db, instance.class(db), constraints) + .negate(db, constraints) } LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes - .when_subclass_of(db, instance.class(db)) - .negate(db), + .when_subclass_of(db, instance.class(db), constraints) + .negate(db, constraints), LiteralValueTypeKind::Enum(enum_literal) => enum_literal .enum_class_instance(db) .has_relation_to_impl( db, Type::NominalInstance(instance), + constraints, inferable, TypeRelation::Subtyping, relation_visitor, disjointness_visitor, ) - .negate(db), + .negate(db, constraints), } } @@ -2233,14 +2475,18 @@ impl<'db> Type<'db> { // A boolean literal must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) KnownClass::Bool - .when_subclass_of(db, instance.class(db)) - .negate(db) + .when_subclass_of(db, instance.class(db), constraints) + .negate(db, constraints) } (Type::TypeIs(_) | Type::TypeGuard(_), _) - | (_, Type::TypeIs(_) | Type::TypeGuard(_)) => ConstraintSet::from(true), + | (_, Type::TypeIs(_) | Type::TypeGuard(_)) => { + ConstraintSet::from_bool(constraints, true) + } - (Type::LiteralValue(_), _) | (_, Type::LiteralValue(_)) => ConstraintSet::from(true), + (Type::LiteralValue(_), _) | (_, Type::LiteralValue(_)) => { + ConstraintSet::from_bool(constraints, true) + } // A class-literal type `X` is always disjoint from an instance type `Y`, // unless the type expressing "all instances of `Z`" is a subtype of of `Y`, @@ -2248,8 +2494,8 @@ impl<'db> Type<'db> { (Type::ClassLiteral(class), instance @ Type::NominalInstance(_)) | (instance @ Type::NominalInstance(_), Type::ClassLiteral(class)) => class .metaclass_instance_type(db) - .when_subtype_of(db, instance, inferable) - .negate(db), + .when_subtype_of(db, instance, constraints, inferable) + .negate(db, constraints), (Type::GenericAlias(alias), instance @ Type::NominalInstance(_)) | (instance @ Type::NominalInstance(_), Type::GenericAlias(alias)) => { ClassType::from(alias) @@ -2257,12 +2503,13 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, instance, + constraints, inferable, TypeRelation::Subtyping, relation_visitor, disjointness_visitor, ) - .negate(db) + .negate(db, constraints) } (Type::FunctionLiteral(..), Type::NominalInstance(instance)) @@ -2270,8 +2517,8 @@ impl<'db> Type<'db> { // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // (it cannot be an instance of a `types.FunctionType` subclass) KnownClass::FunctionType - .when_subclass_of(db, instance.class(db)) - .negate(db) + .when_subclass_of(db, instance.class(db), constraints) + .negate(db, constraints) } (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType @@ -2279,6 +2526,7 @@ impl<'db> Type<'db> { .is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2288,6 +2536,7 @@ impl<'db> Type<'db> { method.class().to_instance(db).is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2300,6 +2549,7 @@ impl<'db> Type<'db> { .is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2311,7 +2561,7 @@ impl<'db> Type<'db> { // No two callable types are ever disjoint because // `(*args: object, **kwargs: object) -> Never` is a subtype of all fully static // callable types. - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } (Type::Callable(_), Type::SpecialForm(special_form)) @@ -2320,7 +2570,7 @@ impl<'db> Type<'db> { // that are callable (like TypedDict and collection constructors). // Most special forms are type constructors/annotations (like `typing.Literal`, // `typing.Union`, etc.) that are subscripted, not called. - ConstraintSet::from(!special_form.is_callable()) + ConstraintSet::from_bool(constraints, !special_form.is_callable()) } ( @@ -2338,17 +2588,18 @@ impl<'db> Type<'db> { ) .place .ignore_possibly_undefined() - .when_none_or(|dunder_call| { + .when_none_or(db, constraints, |dunder_call| { dunder_call .has_relation_to_impl( db, Type::Callable(CallableType::unknown(db)), + constraints, inferable, TypeRelation::Assignability, relation_visitor, disjointness_visitor, ) - .negate(db) + .negate(db, constraints) }), ( @@ -2360,7 +2611,7 @@ impl<'db> Type<'db> { Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), ) => { // TODO: Implement disjointness for general callable type with other types - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } (Type::ModuleLiteral(..), other @ Type::NominalInstance(..)) @@ -2369,6 +2620,7 @@ impl<'db> Type<'db> { other.is_disjoint_from_impl( db, KnownClass::ModuleType.to_instance(db), + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2380,6 +2632,7 @@ impl<'db> Type<'db> { left.is_disjoint_from_impl( db, right, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2387,12 +2640,13 @@ impl<'db> Type<'db> { }), (Type::NewTypeInstance(left), Type::NewTypeInstance(right)) => { - left.is_disjoint_from_impl(db, right) + left.is_disjoint_from_impl(db, right, constraints) } (Type::NewTypeInstance(newtype), other) | (other, Type::NewTypeInstance(newtype)) => { newtype.concrete_base_type(db).is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2403,6 +2657,7 @@ impl<'db> Type<'db> { KnownClass::Property.to_instance(db).is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2410,25 +2665,35 @@ impl<'db> Type<'db> { } (Type::BoundSuper(left), Type::BoundSuper(right)) => left - .is_equivalent_to_impl(db, right, relation_visitor, disjointness_visitor) - .negate(db), + .is_equivalent_to_impl( + db, + right, + constraints, + relation_visitor, + disjointness_visitor, + ) + .negate(db, constraints), (Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => { KnownClass::Super.to_instance(db).is_disjoint_from_impl( db, other, + constraints, inferable, disjointness_visitor, relation_visitor, ) } - (Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => ConstraintSet::from(true), + (Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => { + ConstraintSet::from_bool(constraints, true) + } (Type::TypedDict(self_typeddict), Type::TypedDict(other_typeddict)) => { disjointness_visitor.visit((self, other), || { self_typeddict.is_disjoint_from_impl( db, other_typeddict, + constraints, inferable, disjointness_visitor, relation_visitor, @@ -2445,46 +2710,47 @@ impl<'db> Type<'db> { .has_relation_to_impl( db, other, + constraints, inferable, TypeRelation::Assignability, relation_visitor, disjointness_visitor, ) - .negate(db), + .negate(db, constraints), } } } /// A [`PairVisitor`] that is used in `has_relation_to` methods. -pub(crate) type HasRelationToVisitor<'db> = - CycleDetector, (Type<'db>, Type<'db>, TypeRelation<'db>), ConstraintSet<'db>>; +pub(crate) type HasRelationToVisitor<'db, 'c> = CycleDetector< + TypeRelation, + (Type<'db>, Type<'db>, TypeRelation), + ConstraintSet<'db, 'c>, + ConstraintSet<'db, 'c>, +>; + +impl<'db, 'c> HasRelationToVisitor<'db, 'c> { + pub(crate) fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { + HasRelationToVisitor::with_given(constraints, ConstraintSet::from_bool(constraints, false)) + } -impl Default for HasRelationToVisitor<'_> { - fn default() -> Self { - HasRelationToVisitor::new(ConstraintSet::from(true)) + pub(crate) fn with_given( + constraints: &'c ConstraintSetBuilder<'db>, + given: ConstraintSet<'db, 'c>, + ) -> Self { + let fallback = ConstraintSet::from_bool(constraints, true); + HasRelationToVisitor::with_extra(fallback, given) } } /// A [`PairVisitor`] that is used in `is_disjoint_from` methods. -pub(crate) type IsDisjointVisitor<'db> = PairVisitor<'db, IsDisjoint, ConstraintSet<'db>>; +pub(crate) type IsDisjointVisitor<'db, 'c> = PairVisitor<'db, IsDisjoint, ConstraintSet<'db, 'c>>; #[derive(Debug)] pub(crate) struct IsDisjoint; -impl Default for IsDisjointVisitor<'_> { - fn default() -> Self { - IsDisjointVisitor::new(ConstraintSet::from(false)) - } -} - -/// A [`PairVisitor`] that is used in `is_equivalent` methods. -pub(crate) type IsEquivalentVisitor<'db> = PairVisitor<'db, IsEquivalent, ConstraintSet<'db>>; - -#[derive(Debug)] -pub(crate) struct IsEquivalent; - -impl Default for IsEquivalentVisitor<'_> { - fn default() -> Self { - IsEquivalentVisitor::new(ConstraintSet::from(true)) +impl<'db, 'c> IsDisjointVisitor<'db, 'c> { + pub(crate) fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { + IsDisjointVisitor::new(ConstraintSet::from_bool(constraints, false)) } } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 753c75533ebc0..cac7f1caf9242 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -18,7 +18,9 @@ use smallvec::{SmallVec, smallvec_inline}; use super::{DynamicType, Type, TypeVarVariance, semantic_index}; use crate::semantic_index::definition::Definition; -use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; +use crate::types::constraints::{ + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, +}; use crate::types::generics::{GenericContext, InferableTypeVars, walk_generic_context}; use crate::types::infer::infer_deferred_types; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; @@ -286,19 +288,22 @@ impl<'db> CallableSignature<'db> { } } - pub(crate) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(crate) fn has_relation_to_impl<'c>( &self, db: &'db dyn Db, other: &Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { Self::has_relation_to_inner( db, &self.overloads, &other.overloads, + constraints, inferable, relation, relation_visitor, @@ -326,19 +331,21 @@ impl<'db> CallableSignature<'db> { .map(|bound_typevar| (bound_typevar, signature.return_ty)) } - pub(crate) fn when_constraint_set_assignable_to( + pub(crate) fn when_constraint_set_assignable_to<'c>( &self, db: &'db dyn Db, other: &Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { + ) -> ConstraintSet<'db, 'c> { self.has_relation_to_impl( db, other, + constraints, inferable, TypeRelation::ConstraintSetAssignability, - &HasRelationToVisitor::default(), - &IsDisjointVisitor::default(), + &HasRelationToVisitor::default(constraints), + &IsDisjointVisitor::default(constraints), ) } @@ -347,13 +354,14 @@ impl<'db> CallableSignature<'db> { /// /// This is intentionally accept-only. If the probe does not definitely succeed, it returns /// `None` and callers should fall back to legacy per-overload relation checks. - fn try_unary_overload_aggregate_relation( + fn try_unary_overload_aggregate_relation<'c>( db: &'db dyn Db, + constraints: &'c ConstraintSetBuilder<'db>, self_signatures: &[Signature<'db>], other_signature: &Signature<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - ) -> Option> { + relation: TypeRelation, + ) -> Option> { let single_required_positional_parameter_type = |signature: &Signature<'db>| { if signature.parameters().len() != 1 { return None; @@ -402,7 +410,7 @@ impl<'db> CallableSignature<'db> { return None; } let signatures_are_disjoint = self_parameter_type - .when_disjoint_from(db, other_parameter_type, inferable) + .when_disjoint_from(db, other_parameter_type, constraints, inferable) .is_always_satisfied(db); if signatures_are_disjoint { @@ -422,16 +430,19 @@ impl<'db> CallableSignature<'db> { let parameters_cover_target = other_parameter_type.has_relation_to( db, parameter_type_union.build(), + constraints, inferable, relation, ); let returns_match_target = return_type_union.build().has_relation_to( db, other_signature.return_ty, + constraints, inferable, relation, ); - let aggregate_relation = parameters_cover_target.and(db, || returns_match_target); + let aggregate_relation = + parameters_cover_target.and(db, constraints, || returns_match_target); aggregate_relation .is_always_satisfied(db) .then_some(aggregate_relation) @@ -439,15 +450,17 @@ impl<'db> CallableSignature<'db> { /// Implementation of subtyping and assignability between two, possible overloaded, callable /// types. - fn has_relation_to_inner( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_inner<'c>( db: &'db dyn Db, self_signatures: &[Signature<'db>], other_signatures: &[Signature<'db>], + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { if relation.is_constraint_set_assignability() { // TODO: Oof, maybe ParamSpec needs to live at CallableSignature, not Signature? let self_is_single_paramspec = Self::signatures_is_single_paramspec(self_signatures); @@ -464,6 +477,7 @@ impl<'db> CallableSignature<'db> { ) => { let param_spec_matches = ConstraintSet::constrain_typevar( db, + constraints, self_bound_typevar, Type::TypeVar(other_bound_typevar), Type::TypeVar(other_bound_typevar), @@ -471,12 +485,13 @@ impl<'db> CallableSignature<'db> { let return_types_match = self_return_type.has_relation_to_impl( db, other_return_type, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ); - return param_spec_matches.and(db, || return_types_match); + return param_spec_matches.and(db, constraints, || return_types_match); } (Some((self_bound_typevar, self_return_type)), None) => { @@ -495,6 +510,7 @@ impl<'db> CallableSignature<'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar( db, + constraints, self_bound_typevar, Type::Never, upper, @@ -502,17 +518,18 @@ impl<'db> CallableSignature<'db> { let return_types_match = other_signatures .iter() .map(|signature| signature.return_ty) - .when_any(db, |other_return_type| { + .when_any(db, constraints, |other_return_type| { self_return_type.has_relation_to_impl( db, other_return_type, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) }); - return param_spec_matches.and(db, || return_types_match); + return param_spec_matches.and(db, constraints, || return_types_match); } (None, Some((other_bound_typevar, other_return_type))) => { @@ -531,6 +548,7 @@ impl<'db> CallableSignature<'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar( db, + constraints, other_bound_typevar, lower, Type::object(), @@ -538,17 +556,18 @@ impl<'db> CallableSignature<'db> { let return_types_match = self_signatures .iter() .map(|signature| signature.return_ty) - .when_any(db, |self_return_type| { + .when_any(db, constraints, |self_return_type| { self_return_type.has_relation_to_impl( db, other_return_type, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) }); - return param_spec_matches.and(db, || return_types_match); + return param_spec_matches.and(db, constraints, || return_types_match); } (None, None) => {} @@ -561,6 +580,7 @@ impl<'db> CallableSignature<'db> { self_signature.has_relation_to_impl( db, other_signature, + constraints, inferable, relation, relation_visitor, @@ -572,6 +592,7 @@ impl<'db> CallableSignature<'db> { (_, [other_signature]) => { if let Some(aggregate_relation) = Self::try_unary_overload_aggregate_relation( db, + constraints, self_signatures, other_signature, inferable, @@ -580,44 +601,53 @@ impl<'db> CallableSignature<'db> { return aggregate_relation; } - self_signatures.iter().when_any(db, |self_signature| { + self_signatures + .iter() + .when_any(db, constraints, |self_signature| { + Self::has_relation_to_inner( + db, + std::slice::from_ref(self_signature), + other_signatures, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } + + // `self` is definitely not overloaded while `other` is possibly overloaded. + ([_], _) => other_signatures + .iter() + .when_all(db, constraints, |other_signature| { Self::has_relation_to_inner( db, - std::slice::from_ref(self_signature), - other_signatures, + self_signatures, + std::slice::from_ref(other_signature), + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - }) - } - - // `self` is definitely not overloaded while `other` is possibly overloaded. - ([_], _) => other_signatures.iter().when_all(db, |other_signature| { - Self::has_relation_to_inner( - db, - self_signatures, - std::slice::from_ref(other_signature), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), + }), // `self` is definitely overloaded while `other` is possibly overloaded. - (_, _) => other_signatures.iter().when_all(db, |other_signature| { - Self::has_relation_to_inner( - db, - self_signatures, - std::slice::from_ref(other_signature), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), + (_, _) => other_signatures + .iter() + .when_all(db, constraints, |other_signature| { + Self::has_relation_to_inner( + db, + self_signatures, + std::slice::from_ref(other_signature), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }), } } } @@ -1013,12 +1043,13 @@ impl<'db> Signature<'db> { } } - pub(crate) fn when_constraint_set_assignable_to_signatures( + pub(crate) fn when_constraint_set_assignable_to_signatures<'c>( &self, db: &'db dyn Db, other: &CallableSignature<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { + ) -> ConstraintSet<'db, 'c> { // If this signature is a paramspec, bind it to the entire overloaded other callable. if let Some(self_bound_typevar) = self.parameters.as_paramspec() && other.is_single_paramspec().is_none() @@ -1034,53 +1065,66 @@ impl<'db> Signature<'db> { })), CallableTypeKind::ParamSpecValue, )); - let param_spec_matches = - ConstraintSet::constrain_typevar(db, self_bound_typevar, Type::Never, upper); + let param_spec_matches = ConstraintSet::constrain_typevar( + db, + constraints, + self_bound_typevar, + Type::Never, + upper, + ); let return_types_match = other .overloads .iter() .map(|signature| signature.return_ty) - .when_any(db, |other_return_type| { + .when_any(db, constraints, |other_return_type| { self.return_ty.when_constraint_set_assignable_to( db, other_return_type, + constraints, inferable, ) }); - return param_spec_matches.and(db, || return_types_match); + return param_spec_matches.and(db, constraints, || return_types_match); } - other.overloads.iter().when_all(db, |other_signature| { - self.when_constraint_set_assignable_to(db, other_signature, inferable) - }) + other + .overloads + .iter() + .when_all(db, constraints, |other_signature| { + self.when_constraint_set_assignable_to(db, other_signature, constraints, inferable) + }) } - fn when_constraint_set_assignable_to( + fn when_constraint_set_assignable_to<'c>( &self, db: &'db dyn Db, other: &Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db> { + ) -> ConstraintSet<'db, 'c> { self.has_relation_to_impl( db, other, + constraints, inferable, TypeRelation::ConstraintSetAssignability, - &HasRelationToVisitor::default(), - &IsDisjointVisitor::default(), + &HasRelationToVisitor::default(constraints), + &IsDisjointVisitor::default(constraints), ) } /// Implementation of subtyping and assignability for signature. - fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_impl<'c>( &self, db: &'db dyn Db, other: &Signature<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { // If either signature is generic, their typevars should also be considered inferable when // checking whether one signature is a subtype/etc of the other, since we only need to find // one specialization that causes the check to succeed. @@ -1102,6 +1146,7 @@ impl<'db> Signature<'db> { let when = self.has_relation_to_inner( db, other, + constraints, inferable, relation, relation_visitor, @@ -1112,18 +1157,24 @@ impl<'db> Signature<'db> { // we produce, we reduce it back down to the inferable set that the caller asked about. // If we introduced new inferable typevars, those will be existentially quantified away // before returning. - when.reduce_inferable(db, self_inferable.iter().chain(other_inferable.iter())) + when.reduce_inferable( + db, + constraints, + self_inferable.iter().chain(other_inferable.iter()), + ) } - fn has_relation_to_inner( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_inner<'c>( &self, db: &'db dyn Db, other: &Signature<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { /// A helper struct to zip two slices of parameters together that provides control over the /// two iterators individually. It also keeps track of the current parameter in each /// iterator. @@ -1185,7 +1236,7 @@ impl<'db> Signature<'db> { } } - let mut result = ConstraintSet::from(true); + let mut result = ConstraintSet::from_bool(constraints, true); let mut check_types = |type1: Type<'db>, type2: Type<'db>| { match (type1, type2) { @@ -1213,9 +1264,11 @@ impl<'db> Signature<'db> { !result .intersect( db, + constraints, type1.has_relation_to_impl( db, type2, + constraints, inferable, relation, relation_visitor, @@ -1243,27 +1296,29 @@ impl<'db> Signature<'db> { .keyword_variadic() .is_some_and(|(_, param)| param.annotated_type().is_object()) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } // The top signature is supertype of (and assignable from) all other signatures. It is a // subtype of no signature except itself, and assignable only to the gradual signature. if other.parameters.is_top() { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } else if self.parameters.is_top() && !other.parameters.is_gradual() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } // If either of the parameter lists is gradual (`...`), then it is assignable to and from // any other parameter list, but not a subtype or supertype of any other parameter list. if self.parameters.is_gradual() || other.parameters.is_gradual() { return match relation { - TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => { - ConstraintSet::from(false) + TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => { + ConstraintSet::from_bool(constraints, false) } TypeRelation::Redundancy { .. } => result.intersect( db, - ConstraintSet::from( + constraints, + ConstraintSet::from_bool( + constraints, self.parameters.is_gradual() && other.parameters.is_gradual(), ), ), @@ -1281,11 +1336,12 @@ impl<'db> Signature<'db> { (Some(self_bound_typevar), Some(other_bound_typevar)) => { let param_spec_matches = ConstraintSet::constrain_typevar( db, + constraints, self_bound_typevar, Type::TypeVar(other_bound_typevar), Type::TypeVar(other_bound_typevar), ); - result.intersect(db, param_spec_matches); + result.intersect(db, constraints, param_spec_matches); return result; } @@ -1301,11 +1357,12 @@ impl<'db> Signature<'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar( db, + constraints, self_bound_typevar, Type::Never, upper, ); - result.intersect(db, param_spec_matches); + result.intersect(db, constraints, param_spec_matches); return result; } @@ -1321,11 +1378,12 @@ impl<'db> Signature<'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar( db, + constraints, other_bound_typevar, lower, Type::object(), ); - result.intersect(db, param_spec_matches); + result.intersect(db, constraints, param_spec_matches); return result; } @@ -1368,7 +1426,7 @@ impl<'db> Signature<'db> { // `other`, then the non-variadic parameters in `self` must have a default // value. if default_type.is_none() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } } ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => { @@ -1380,7 +1438,7 @@ impl<'db> Signature<'db> { EitherOrBoth::Right(_) => { // If there are more parameters in `other` than in `self`, then `self` is not a // subtype of `other`. - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } EitherOrBoth::Both(self_parameter, other_parameter) => { @@ -1400,7 +1458,7 @@ impl<'db> Signature<'db> { }, ) => { if self_default.is_none() && other_default.is_some() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } if !check_types( other_parameter.annotated_type(), @@ -1421,11 +1479,11 @@ impl<'db> Signature<'db> { }, ) => { if self_name != other_name { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } // The following checks are the same as positional-only parameters. if self_default.is_none() && other_default.is_some() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } if !check_types( other_parameter.annotated_type(), @@ -1510,7 +1568,7 @@ impl<'db> Signature<'db> { break; } - _ => return ConstraintSet::from(false), + _ => return ConstraintSet::from_bool(constraints, false), } } } @@ -1544,7 +1602,7 @@ impl<'db> Signature<'db> { // only contains keyword-only and keyword-variadic parameters. However, if the // parameter has a default, it's valid because callers don't need to provide it. if default_type.is_none() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } } ParameterKind::Variadic { .. } => {} @@ -1572,7 +1630,7 @@ impl<'db> Signature<'db> { .. } => { if self_default.is_none() && other_default.is_some() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } if !check_types( other_parameter.annotated_type(), @@ -1593,14 +1651,14 @@ impl<'db> Signature<'db> { return result; } } else { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } } ParameterKind::KeywordVariadic { .. } => { let Some(self_keyword_variadic_type) = self_keyword_variadic else { // For a `self <: other` relationship, if `other` has a keyword variadic // parameter, `self` must also have a keyword variadic parameter. - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); }; if !check_types(other_parameter.annotated_type(), self_keyword_variadic_type) { return result; @@ -1608,7 +1666,7 @@ impl<'db> Signature<'db> { } _ => { // This can only occur in case of a syntax error. - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } } } @@ -1617,7 +1675,7 @@ impl<'db> Signature<'db> { // optional otherwise the subtype relation is invalid. for (_, self_parameter) in self_keywords { if self_parameter.default_type().is_none() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } } diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 29ccdf0b97a3c..6e244ebb8233b 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,6 +1,6 @@ use crate::place::PlaceAndQualifiers; use crate::semantic_index::definition::Definition; -use crate::types::constraints::ConstraintSet; +use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; use crate::types::generics::InferableTypeVars; use crate::types::protocol_class::ProtocolClass; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; @@ -212,24 +212,29 @@ impl<'db> SubclassOfType<'db> { } /// Return `true` if `self` has a certain relation to `other`. - pub(crate) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(crate) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: SubclassOfType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match (self.subclass_of, other.subclass_of) { (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from(!relation.is_subtyping()) + ConstraintSet::from_bool(constraints, !relation.is_subtyping()) } (SubclassOfInner::Dynamic(_), SubclassOfInner::Class(other_class)) => { - ConstraintSet::from(other_class.is_object(db) || relation.is_assignability()) + ConstraintSet::from_bool( + constraints, + other_class.is_object(db) || relation.is_assignability(), + ) } (SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from(relation.is_assignability()) + ConstraintSet::from_bool(constraints, relation.is_assignability()) } // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, @@ -239,6 +244,7 @@ impl<'db> SubclassOfType<'db> { .has_relation_to_impl( db, other_class, + constraints, inferable, relation, relation_visitor, @@ -254,19 +260,23 @@ impl<'db> SubclassOfType<'db> { /// Return` true` if `self` is a disjoint type from `other`. /// /// See [`Type::is_disjoint_from`] for more details. - pub(crate) fn is_disjoint_from_impl( + pub(crate) fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, _inferable: InferableTypeVars<'_, 'db>, - _visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + _visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match (self.subclass_of, other.subclass_of) { (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { - ConstraintSet::from(!self_class.could_coexist_in_mro_with(db, other_class)) + ConstraintSet::from_bool( + constraints, + !self_class.could_coexist_in_mro_with(db, other_class, constraints), + ) } (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { unreachable!() diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 8c3155f5c0e97..b35646e53ca60 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -26,7 +26,9 @@ use crate::semantic_index::definition::Definition; use crate::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; use crate::types::builder::RecursivelyDefined; use crate::types::class::{ClassType, KnownClass}; -use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; +use crate::types::constraints::{ + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, +}; use crate::types::generics::InferableTypeVars; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::{ @@ -256,18 +258,21 @@ impl<'db> TupleType<'db> { .find_legacy_typevars_impl(db, binding_context, typevars, visitor); } - pub(crate) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(crate) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { self.tuple(db).has_relation_to_impl( db, other.tuple(db), + constraints, inferable, relation, relation_visitor, @@ -275,17 +280,19 @@ impl<'db> TupleType<'db> { ) } - pub(crate) fn is_disjoint_from_impl( + pub(crate) fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - ) -> ConstraintSet<'db> { + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { self.tuple(db).is_disjoint_from_impl( db, other.tuple(db), + constraints, inferable, disjointness_visitor, relation_visitor, @@ -471,50 +478,56 @@ impl<'db> FixedLengthTuple> { } } - fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_impl<'c>( &self, db: &'db dyn Db, other: &Tuple>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match other { - Tuple::Fixed(other) => { - ConstraintSet::from(self.0.len() == other.0.len()).and(db, || { - (self.0.iter().zip(&other.0)).when_all(db, |(self_ty, other_ty)| { - self_ty.has_relation_to_impl( - db, - *other_ty, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + Tuple::Fixed(other) => ConstraintSet::from_bool( + constraints, + self.0.len() == other.0.len(), + ) + .and(db, constraints, || { + (self.0.iter().zip(&other.0)).when_all(db, constraints, |(self_ty, other_ty)| { + self_ty.has_relation_to_impl( + db, + *other_ty, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) }) - } + }), Tuple::Variable(other) => { // This tuple must have enough elements to match up with the other tuple's prefix // and suffix, and each of those elements must pairwise satisfy the relation. - let mut result = ConstraintSet::from(true); + let mut result = ConstraintSet::from_bool(constraints, true); let mut self_iter = self.0.iter(); for other_ty in other.prefix_elements() { let Some(self_ty) = self_iter.next() else { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); }; let element_constraints = self_ty.has_relation_to_impl( db, *other_ty, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ); if result - .intersect(db, element_constraints) + .intersect(db, constraints, element_constraints) .is_never_satisfied(db) { return result; @@ -522,18 +535,19 @@ impl<'db> FixedLengthTuple> { } for other_ty in other.iter_suffix_elements().rev() { let Some(self_ty) = self_iter.next_back() else { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); }; let element_constraints = self_ty.has_relation_to_impl( db, other_ty, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ); if result - .intersect(db, element_constraints) + .intersect(db, constraints, element_constraints) .is_never_satisfied(db) { return result; @@ -542,11 +556,12 @@ impl<'db> FixedLengthTuple> { // In addition, any remaining elements in this tuple must satisfy the // variable-length portion of the other tuple. - result.and(db, || { - self_iter.when_all(db, |self_ty| { + result.and(db, constraints, || { + self_iter.when_all(db, constraints, |self_ty| { self_ty.has_relation_to_impl( db, other.variable(), + constraints, inferable, relation, relation_visitor, @@ -952,15 +967,17 @@ impl<'db> VariableLengthTuple> { } } - fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_impl<'c>( &self, db: &'db dyn Db, other: &Tuple>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match other { Tuple::Fixed(other) => { // The `...` length specifier of a variable-length tuple type is interpreted @@ -974,28 +991,29 @@ impl<'db> VariableLengthTuple> { // possible lengths. This means that `tuple[Any, ...]` can match any tuple of any // length. if !relation.is_assignability() || !self.variable().is_dynamic() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } // In addition, the other tuple must have enough elements to match up with this // tuple's prefix and suffix, and each of those elements must pairwise satisfy the // relation. - let mut result = ConstraintSet::from(true); + let mut result = ConstraintSet::from_bool(constraints, true); let mut other_iter = other.iter_all_elements(); for self_ty in self.prenormalized_prefix_elements(db, None) { let Some(other_ty) = other_iter.next() else { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); }; let element_constraints = self_ty.has_relation_to_impl( db, other_ty, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ); if result - .intersect(db, element_constraints) + .intersect(db, constraints, element_constraints) .is_never_satisfied(db) { return result; @@ -1004,18 +1022,19 @@ impl<'db> VariableLengthTuple> { let suffix: Vec<_> = self.prenormalized_suffix_elements(db, None).collect(); for self_ty in suffix.iter().rev() { let Some(other_ty) = other_iter.next_back() else { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); }; let element_constraints = self_ty.has_relation_to_impl( db, other_ty, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ); if result - .intersect(db, element_constraints) + .intersect(db, constraints, element_constraints) .is_never_satisfied(db) { return result; @@ -1040,7 +1059,7 @@ impl<'db> VariableLengthTuple> { // The overlapping parts of the prefixes and suffixes must satisfy the relation. // Any remaining parts must satisfy the relation with the other tuple's // variable-length part. - let mut result = ConstraintSet::from(true); + let mut result = ConstraintSet::from_bool(constraints, true); let pairwise = self .prenormalized_prefix_elements(db, self_prenormalize_variable) .zip_longest( @@ -1051,6 +1070,7 @@ impl<'db> VariableLengthTuple> { EitherOrBoth::Both(self_ty, other_ty) => self_ty.has_relation_to_impl( db, other_ty, + constraints, inferable, relation, relation_visitor, @@ -1059,6 +1079,7 @@ impl<'db> VariableLengthTuple> { EitherOrBoth::Left(self_ty) => self_ty.has_relation_to_impl( db, other.variable(), + constraints, inferable, relation, relation_visitor, @@ -1070,11 +1091,12 @@ impl<'db> VariableLengthTuple> { // that can materialize to provide it (for assignability only), // as in `tuple[Any, ...]` matching `tuple[int, int]`. if !relation.is_assignability() || !self.variable().is_dynamic() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } self.variable().has_relation_to_impl( db, other_ty, + constraints, inferable, relation, relation_visitor, @@ -1083,7 +1105,7 @@ impl<'db> VariableLengthTuple> { } }; if result - .intersect(db, pair_constraints) + .intersect(db, constraints, pair_constraints) .is_never_satisfied(db) { return result; @@ -1105,6 +1127,7 @@ impl<'db> VariableLengthTuple> { EitherOrBoth::Both(self_ty, other_ty) => self_ty.has_relation_to_impl( db, *other_ty, + constraints, inferable, relation, relation_visitor, @@ -1113,6 +1136,7 @@ impl<'db> VariableLengthTuple> { EitherOrBoth::Left(self_ty) => self_ty.has_relation_to_impl( db, other.variable(), + constraints, inferable, relation, relation_visitor, @@ -1124,11 +1148,12 @@ impl<'db> VariableLengthTuple> { // that can materialize to provide it (for assignability only), // as in `tuple[Any, ...]` matching `tuple[int, int]`. if !relation.is_assignability() || !self.variable().is_dynamic() { - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } self.variable().has_relation_to_impl( db, *other_ty, + constraints, inferable, relation, relation_visitor, @@ -1137,7 +1162,7 @@ impl<'db> VariableLengthTuple> { } }; if result - .intersect(db, pair_constraints) + .intersect(db, constraints, pair_constraints) .is_never_satisfied(db) { return result; @@ -1145,10 +1170,11 @@ impl<'db> VariableLengthTuple> { } // And lastly, the variable-length portions must satisfy the relation. - result.and(db, || { + result.and(db, constraints, || { self.variable().has_relation_to_impl( db, other.variable(), + constraints, inferable, relation, relation_visitor, @@ -1370,19 +1396,22 @@ impl<'db> Tuple> { } } - fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + fn has_relation_to_impl<'c>( &self, db: &'db dyn Db, other: &Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { match self { Tuple::Fixed(self_tuple) => self_tuple.has_relation_to_impl( db, other, + constraints, inferable, relation, relation_visitor, @@ -1391,6 +1420,7 @@ impl<'db> Tuple> { Tuple::Variable(self_tuple) => self_tuple.has_relation_to_impl( db, other, + constraints, inferable, relation, relation_visitor, @@ -1399,63 +1429,67 @@ impl<'db> Tuple> { } } - pub(super) fn is_disjoint_from_impl( + pub(super) fn is_disjoint_from_impl<'c>( &self, db: &'db dyn Db, other: &Self, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - ) -> ConstraintSet<'db> { + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { // Two tuples with an incompatible number of required elements must always be disjoint. let (self_min, self_max) = self.len().size_hint(); let (other_min, other_max) = other.len().size_hint(); if self_max.is_some_and(|max| max < other_min) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } if other_max.is_some_and(|max| max < self_min) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } // If any of the required elements are pairwise disjoint, the tuples are disjoint as well. #[allow(clippy::items_after_statements)] - fn any_disjoint<'s, 'db>( + #[expect(clippy::too_many_arguments)] + fn any_disjoint<'s, 'db, 'c>( db: &'db dyn Db, a: &'s [Type<'db>], b: &'s [Type<'db>], + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, rev: bool, - ) -> ConstraintSet<'db> + ) -> ConstraintSet<'db, 'c> where 'db: 's, { if rev { - a.iter() - .rev() - .zip(b.iter().rev()) - .when_any(db, |(self_element, other_element)| { + std::iter::zip(a.iter().rev(), b.iter().rev()).when_any( + db, + constraints, + |(self_element, other_element)| { self_element.is_disjoint_from_impl( db, *other_element, + constraints, inferable, disjointness_visitor, relation_visitor, ) - }) + }, + ) } else { - a.iter() - .zip(b) - .when_any(db, |(self_element, other_element)| { - self_element.is_disjoint_from_impl( - db, - *other_element, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) + std::iter::zip(a, b).when_any(db, constraints, |(self_element, other_element)| { + self_element.is_disjoint_from_impl( + db, + *other_element, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ) + }) } } @@ -1464,6 +1498,7 @@ impl<'db> Tuple> { db, self_tuple.all_elements(), other_tuple.all_elements(), + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1477,16 +1512,18 @@ impl<'db> Tuple> { db, self_tuple.prefix_elements(), other_tuple.prefix_elements(), + constraints, inferable, disjointness_visitor, relation_visitor, false, ) - .or(db, || { + .or(db, constraints, || { any_disjoint( db, self_tuple.suffix_elements(), other_tuple.suffix_elements(), + constraints, inferable, disjointness_visitor, relation_visitor, @@ -1499,16 +1536,18 @@ impl<'db> Tuple> { db, fixed.all_elements(), variable.prefix_elements(), + constraints, inferable, disjointness_visitor, relation_visitor, false, ) - .or(db, || { + .or(db, constraints, || { any_disjoint( db, fixed.all_elements(), variable.suffix_elements(), + constraints, inferable, disjointness_visitor, relation_visitor, diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index c75c608cda593..f97156e50b5b4 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -22,7 +22,9 @@ use crate::semantic_index::definition::Definition; use crate::types::TypeContext; use crate::types::TypeDefinition; use crate::types::class::FieldKind; -use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; +use crate::types::constraints::{ + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, +}; use crate::types::generics::InferableTypeVars; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; @@ -126,15 +128,17 @@ impl<'db> TypedDictType<'db> { // Subtyping between `TypedDict`s follows the algorithm described at: // https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types - pub(super) fn has_relation_to_impl( + #[expect(clippy::too_many_arguments)] + pub(super) fn has_relation_to_impl<'c>( self, db: &'db dyn Db, target: TypedDictType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - ) -> ConstraintSet<'db> { + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { // First do a quick nominal check that (if it succeeds) means that we can avoid // materializing the full `TypedDict` schema for either `self` or `target`. // This should be cheaper in many cases, and also helps us avoid some cycles. @@ -142,24 +146,24 @@ impl<'db> TypedDictType<'db> { && let Some(target_defining_class) = target.defining_class() && defining_class.is_subclass_of(db, target_defining_class) { - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } let self_items = self.items(db); let target_items = target.items(db); // Many rules violations short-circuit with "never", but asking whether one field is // [relation] to/of another can produce more complicated constraints, and we collect those. - let mut constraints = ConstraintSet::from(true); + let mut result = ConstraintSet::from_bool(constraints, true); for (target_item_name, target_item_field) in target_items { let field_constraints = if target_item_field.is_required() { // required target fields let Some(self_item_field) = self_items.get(target_item_name) else { // Self is missing a required field. - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); }; if !self_item_field.is_required() { // A required field is not required in self. - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } if target_item_field.is_read_only() { // For `ReadOnly[]` fields in the target, the corresponding fields in @@ -169,6 +173,7 @@ impl<'db> TypedDictType<'db> { self_item_field.declared_ty.has_relation_to_impl( db, target_item_field.declared_ty, + constraints, inferable, relation, relation_visitor, @@ -177,7 +182,7 @@ impl<'db> TypedDictType<'db> { } else { if self_item_field.is_read_only() { // A read-only field can't be assigned to a mutable target. - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } // For mutable fields in the target, the relation needs to apply both // ways, or else mutating the target could violate the structural @@ -189,15 +194,17 @@ impl<'db> TypedDictType<'db> { .has_relation_to_impl( db, target_item_field.declared_ty, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { target_item_field.declared_ty.has_relation_to_impl( db, self_item_field.declared_ty, + constraints, inferable, relation, relation_visitor, @@ -218,6 +225,7 @@ impl<'db> TypedDictType<'db> { self_item_field.declared_ty.has_relation_to_impl( db, target_item_field.declared_ty, + constraints, inferable, relation, relation_visitor, @@ -233,6 +241,7 @@ impl<'db> TypedDictType<'db> { Type::object().when_assignable_to( db, target_item_field.declared_ty, + constraints, inferable, ) } @@ -243,12 +252,12 @@ impl<'db> TypedDictType<'db> { if let Some(self_item_field) = self_items.get(target_item_name) { if self_item_field.is_read_only() { // A read-only field can't be assigned to a mutable target. - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } if self_item_field.is_required() { // A required field can't be assigned to a not-required, mutable field // in the target, because `del` is allowed on the target field. - return ConstraintSet::from(false); + return ConstraintSet::from_bool(constraints, false); } // As above, for mutable fields in the target, the relation needs @@ -258,15 +267,17 @@ impl<'db> TypedDictType<'db> { .has_relation_to_impl( db, target_item_field.declared_ty, + constraints, inferable, relation, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { target_item_field.declared_ty.has_relation_to_impl( db, self_item_field.declared_ty, + constraints, inferable, relation, relation_visitor, @@ -280,16 +291,16 @@ impl<'db> TypedDictType<'db> { // interaction between two structural assignability rules prevents // unsoundness" in `typed_dict.md`. // TODO: `closed` and `extra_items` support will go here. - ConstraintSet::from(false) + ConstraintSet::from_bool(constraints, false) } } }; - constraints.intersect(db, field_constraints); - if constraints.is_never_satisfied(db) { - return constraints; + result.intersect(db, constraints, field_constraints); + if result.is_never_satisfied(db) { + return result; } } - constraints + result } pub fn definition(self, db: &'db dyn Db) -> Option> { @@ -365,16 +376,17 @@ impl<'db> TypedDictType<'db> { /// be assignable to both.) /// /// TODO: Adding support for `closed` and `extra_items` will complicate this. - pub(crate) fn is_disjoint_from_impl( + pub(crate) fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: TypedDictType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db>, - relation_visitor: &HasRelationToVisitor<'db>, - ) -> ConstraintSet<'db> { + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { let fields_in_common = btreemap_values_with_same_key(self.items(db), other.items(db)); - fields_in_common.when_any(db, |(self_field, other_field)| { + fields_in_common.when_any(db, constraints, |(self_field, other_field)| { // Condition 1 above. if self_field.is_required() || other_field.is_required() { if (!self_field.is_required() && !self_field.is_read_only()) @@ -382,7 +394,7 @@ impl<'db> TypedDictType<'db> { { // One side demands a `Required` source field, while the other side demands a // `NotRequired` one. They must be disjoint. - return ConstraintSet::from(true); + return ConstraintSet::from_bool(constraints, true); } } if !self_field.is_read_only() && !other_field.is_read_only() { @@ -393,22 +405,24 @@ impl<'db> TypedDictType<'db> { .has_relation_to_impl( db, other_field.declared_ty, + constraints, inferable, TypeRelation::Assignability, relation_visitor, disjointness_visitor, ) - .and(db, || { + .and(db, constraints, || { other_field.declared_ty.has_relation_to_impl( db, self_field.declared_ty, + constraints, inferable, TypeRelation::Assignability, relation_visitor, disjointness_visitor, ) }) - .negate(db) + .negate(db, constraints) } else if !self_field.is_read_only() { // Half of condition 3 above. self_field @@ -416,12 +430,13 @@ impl<'db> TypedDictType<'db> { .has_relation_to_impl( db, other_field.declared_ty, + constraints, inferable, TypeRelation::Assignability, relation_visitor, disjointness_visitor, ) - .negate(db) + .negate(db, constraints) } else if !other_field.is_read_only() { // The other half of condition 3 above. other_field @@ -429,17 +444,19 @@ impl<'db> TypedDictType<'db> { .has_relation_to_impl( db, self_field.declared_ty, + constraints, inferable, TypeRelation::Assignability, relation_visitor, disjointness_visitor, ) - .negate(db) + .negate(db, constraints) } else { // Condition 4 above. self_field.declared_ty.is_disjoint_from_impl( db, other_field.declared_ty, + constraints, inferable, disjointness_visitor, relation_visitor, From b90d4127608909c178a8c467c6c6fa7dae4f8426 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 25 Feb 2026 14:28:59 -0500 Subject: [PATCH 112/261] [ty] Bump version of `lsp-types` This brings in https://github.com/astral-sh/lsp-types/pull/2, which is necessary to advertise an LSP server's capability to provide type hierarchy information. --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2fb4a3ecdd35..094de23b51d8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "lsp-types" version = "0.95.1" -source = "git+https://github.com/astral-sh/lsp-types.git?rev=3512a9f#3512a9f33eadc5402cfab1b8f7340824c8ca1439" +source = "git+https://github.com/astral-sh/lsp-types.git?rev=e15db0593f0ecbbd80599c3f5880e4bf5da1ca0c#e15db0593f0ecbbd80599c3f5880e4bf5da1ca0c" dependencies = [ "bitflags 1.3.2", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3648bf3457f6c..6a6ea32f0a6cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,7 @@ libc = { version = "0.2.153" } libcst = { version = "1.8.4", default-features = false } log = { version = "0.4.17" } lsp-server = { version = "0.7.6" } -lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [ +lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "e15db0593f0ecbbd80599c3f5880e4bf5da1ca0c", features = [ "proposed", ] } matchit = { version = "0.9.0" } From e2d879abf86fa1ffccfa150f1b86527a8a627841 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 25 Feb 2026 14:31:03 -0500 Subject: [PATCH 113/261] [ty] Move some module name helper routines to methods on `ModuleName` These were previously only used in auto-import, but we'll want to use them for filtering subtypes too. So put them in a more central location. I think these codify pretty solid ecosystem conventions, but I've added some cautionary language to the docs for these methods. --- crates/ty_ide/src/all_symbols.rs | 85 ++----------------- crates/ty_module_resolver/src/module_name.rs | 89 ++++++++++++++++++++ 2 files changed, 94 insertions(+), 80 deletions(-) diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index c5a5d946dc34b..7eed9d1f675ab 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -44,6 +44,7 @@ pub fn all_symbols<'db>( let Some(file) = module.file(&*db) else { continue; }; + let name = module.name(&*db); // Note that this will always consider namespace // packages to be "not firsty party." This isn't @@ -55,33 +56,15 @@ pub fn all_symbols<'db>( .search_path(&*db) .is_none_or(|sp| !sp.is_first_party()); - // By convention, modules starting with an underscore - // are generally considered unexported. However, we - // should consider first party modules fair game. - // - // Note that we apply this recursively. e.g., - // `numpy._core.multiarray` is considered private - // because it's a child of `_core`. - if is_non_first_party && module.name(&*db).components().any(|c| c.starts_with('_')) - { + // Filter out non-first-party modules that are conventionally + // regarded as private or tests. + if is_non_first_party && (name.is_private() || name.is_test_module()) { continue; } - // Test modules in third-party packages are almost never - // useful to import. We filter out: - // - Modules where a non-root component is "test" or "tests" - // (e.g., `numpy.tests.test_core`) - // - Modules named "conftest" (pytest configuration) - // - // Note: We intentionally keep top-level "testing" modules - // like `pandas.testing` since those provide utilities meant - // for external use. - if is_non_first_party && is_test_module(module.name(&*db)) { - continue; - } // TODO: also make it available in `TYPE_CHECKING` blocks // (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well) - if !is_typing_extensions_available && module.name(&*db) == &typing_extensions { + if !is_typing_extensions_available && name == &typing_extensions { continue; } s.spawn(move |_| { @@ -576,30 +559,6 @@ mod merge { } } -/// Returns `true` if the module appears to be a test module. -/// -/// A module is considered a test module if: -/// - Any non-root component is "test" or "tests" (e.g., `numpy.tests.test_core`) -/// - The final component is "conftest" (pytest configuration) -/// -/// Note: Top-level "testing" modules like `pandas.testing` are intentionally -/// not filtered, as they provide utilities meant for external use. -fn is_test_module(module_name: &ModuleName) -> bool { - // Check if the final component is "conftest" (pytest configuration) - if module_name.components().next_back() == Some("conftest") { - return true; - } - - // Check if any non-root component is "test" or "tests" We skip the - // first component since that's usually the name of a PyPI package. - // We generally only want to exclude test modules from *inside* a - // package. - module_name - .components() - .skip(1) - .any(|c| c == "test" || c == "tests") -} - #[cfg(test)] mod tests { use super::*; @@ -1225,38 +1184,4 @@ def test_helper_xyzxyzxyz(): main } } - - #[test] - fn is_test_module_detects_test_directories() { - // Test modules (should be filtered for non-first-party) - assert!(is_test_module( - &ModuleName::new_static("numpy.tests.test_core").unwrap() - )); - assert!(is_test_module( - &ModuleName::new_static("pandas.tests.arithmetic.test_numeric").unwrap() - )); - assert!(is_test_module( - &ModuleName::new_static("requests.test.utils").unwrap() - )); - - // Conftest modules (should be filtered) - assert!(is_test_module( - &ModuleName::new_static("mypackage.conftest").unwrap() - )); - assert!(is_test_module(&ModuleName::new_static("conftest").unwrap())); - - // Non-test modules (should NOT be filtered) - assert!(!is_test_module(&ModuleName::new_static("numpy").unwrap())); - assert!(!is_test_module( - &ModuleName::new_static("pandas.testing").unwrap() - )); - assert!(!is_test_module(&ModuleName::new_static("pytest").unwrap())); - assert!(!is_test_module( - &ModuleName::new_static("unittest").unwrap() - )); - // Root-level test packages should not be filtered - // (the filter only applies to non-root components) - assert!(!is_test_module(&ModuleName::new_static("test").unwrap())); - assert!(!is_test_module(&ModuleName::new_static("tests").unwrap())); - } } diff --git a/crates/ty_module_resolver/src/module_name.rs b/crates/ty_module_resolver/src/module_name.rs index d63576079bf2d..49f2a71784b33 100644 --- a/crates/ty_module_resolver/src/module_name.rs +++ b/crates/ty_module_resolver/src/module_name.rs @@ -339,6 +339,95 @@ impl ModuleName { ) -> Result { Self::from_identifier_parts(db, importing_file, None, 1) } + + /// Returns `true` if the module name given appears to be a test module. + /// + /// This routine is meant to codify a Python ecosystem convention. That is, + /// a module is considered a test module if any of the following are true: + /// + /// * Any non-root component is `test` or `tests` + /// (e.g., `numpy.tests.test_core`). + /// * The final component is `conftest` (pytest configuration). + /// + /// Note that top-level "testing" modules like `pandas.testing` are + /// intentionally not filtered, as they provide utilities meant for external + /// use. + /// + /// # Usage + /// + /// Callers should be mindful when using this routine to filter items + /// presented to end users. For example, auto-import uses this to filter + /// completions offered, but only for completions outside of the end + /// user's first party code. That is, end users still expect to see + /// suggestions from their own test modules, but not for test modules in + /// their dependencies. + /// + /// # Examples + /// + /// ``` + /// use ty_module_resolver::ModuleName; + /// + /// // Some positive examples. + /// let module_name = ModuleName::new_static("numpy.tests").unwrap(); + /// assert!(module_name.is_test_module()); + /// let module_name = ModuleName::new_static("requests.test").unwrap(); + /// assert!(module_name.is_test_module()); + /// let module_name = ModuleName::new_static("conftest").unwrap(); + /// assert!(module_name.is_test_module()); + /// let module_name = ModuleName::new_static("foo.bar.conftest").unwrap(); + /// assert!(module_name.is_test_module()); + /// + /// // Some negative examples. + /// let module_name = ModuleName::new_static("foo.testing").unwrap(); + /// assert!(!module_name.is_test_module()); + /// let module_name = ModuleName::new_static("tests").unwrap(); + /// assert!(!module_name.is_test_module()); + /// let module_name = ModuleName::new_static("test").unwrap(); + /// assert!(!module_name.is_test_module()); + /// let module_name = ModuleName::new_static("pytest").unwrap(); + /// assert!(!module_name.is_test_module()); + /// let module_name = ModuleName::new_static("unittest").unwrap(); + /// assert!(!module_name.is_test_module()); + /// ``` + pub fn is_test_module(&self) -> bool { + if self.components().next_back() == Some("conftest") { + return true; + } + self.components() + .skip(1) + .any(|c| c == "test" || c == "tests") + } + + /// Returns `true` if the module name is considered private. + /// + /// This routine is meant to codify a Python ecosystem convention. That is, + /// a module is considered private if itself or any of its parent modules + /// starts with a `_`. + /// + /// # Usage + /// + /// Callers should be mindful when using this routine to filter items + /// presented to end users. For example, auto-import uses this to filter + /// completions offered, but only for completions outside of the end user's + /// first party code. That is, end users still expect to see suggestions + /// from their private modules, but not for private modules in their + /// dependencies. + /// + /// # Examples + /// + /// ``` + /// use ty_module_resolver::ModuleName; + /// + /// let module_name = ModuleName::new_static("_foo").unwrap(); + /// assert!(module_name.is_private()); + /// let module_name = ModuleName::new_static("foo._bar").unwrap(); + /// assert!(module_name.is_private()); + /// let module_name = ModuleName::new_static("foo._bar.quux").unwrap(); + /// assert!(module_name.is_private()); + /// ``` + pub fn is_private(&self) -> bool { + self.components().any(|c| c.starts_with('_')) + } } impl Deref for ModuleName { From 781e10500bb61bf06770b846f694d487867efdac Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 25 Feb 2026 14:32:42 -0500 Subject: [PATCH 114/261] [ty] Add some helper methods on `ClassLiteral` --- crates/ty_python_semantic/src/types/class.rs | 29 ++++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index c02e75e7e1227..2316040ea8878 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -409,6 +409,14 @@ pub enum ClassLiteral<'db> { } impl<'db> ClassLiteral<'db> { + /// Return a `ClassLiteral` representing the class `builtins.object` + pub(super) fn object(db: &'db dyn Db) -> Self { + KnownClass::Object + .to_class_literal(db) + .as_class_literal() + .expect("`object` should always be a non-generic class in typeshed") + } + /// Returns the name of the class. pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { match self { @@ -756,6 +764,20 @@ impl<'db> ClassLiteral<'db> { Self::DynamicNamedTuple(_) => self, } } + + /// Returns all of the explicit base class types for this class. + /// + /// Note that when this is a namedtuple this always returns a sequence + /// of length one corresponding to `tuple`. + pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + match self { + Self::Static(static_class) => static_class.explicit_bases(db).into(), + Self::Dynamic(dynamic_class) => dynamic_class.explicit_bases(db).into(), + Self::DynamicNamedTuple(namedtuple) => { + [Type::from(namedtuple.tuple_base_class(db))].into() + } + } + } } impl<'db> From> for ClassLiteral<'db> { @@ -795,12 +817,7 @@ pub enum ClassType<'db> { impl<'db> ClassType<'db> { /// Return a `ClassType` representing the class `builtins.object` pub(super) fn object(db: &'db dyn Db) -> Self { - ClassType::NonGeneric( - KnownClass::Object - .to_class_literal(db) - .as_class_literal() - .expect("`object` should always be a non-generic class in typeshed"), - ) + ClassType::NonGeneric(ClassLiteral::object(db)) } pub(super) const fn is_generic(self) -> bool { From 4a75d302e3ce2a049dd80aa691b3a116cedfa4e5 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 25 Feb 2026 14:35:04 -0500 Subject: [PATCH 115/261] [ty] Implement internal routines for providing the LSP "type hierarchy" feature Most of the interesting logic is inside of `ty_python_semantic`. We include a light wrapper API along with tests in `ty_ide`, which I think follows our existing convention for this sort of thing. Some of the tests demonstrate limitations in the current approach. I believe all such limitations are present in pylance as well. So this should bring us to parity at minimum. --- crates/ty_ide/src/lib.rs | 4 + crates/ty_ide/src/type_hierarchy.rs | 729 ++++++++++++++++++ crates/ty_python_semantic/src/lib.rs | 7 +- .../ty_python_semantic/src/semantic_index.rs | 2 +- .../src/types/ide_support.rs | 244 +++++- 5 files changed, 980 insertions(+), 6 deletions(-) create mode 100644 crates/ty_ide/src/type_hierarchy.rs diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index bc095df78b75c..ca04a62f5fb89 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -25,6 +25,7 @@ mod semantic_tokens; mod signature_help; mod stub_mapping; mod symbols; +mod type_hierarchy; mod workspace_symbols; pub use all_symbols::{AllSymbolInfo, all_symbols}; @@ -48,6 +49,9 @@ pub use semantic_tokens::{ }; pub use signature_help::{ParameterDetails, SignatureDetails, SignatureHelpInfo, signature_help}; pub use symbols::{FlatSymbols, HierarchicalSymbols, SymbolId, SymbolInfo, SymbolKind}; +pub use type_hierarchy::{ + TypeHierarchyItem, prepare_type_hierarchy, type_hierarchy_subtypes, type_hierarchy_supertypes, +}; pub use workspace_symbols::{WorkspaceSymbolInfo, workspace_symbols}; use ruff_db::{ diff --git a/crates/ty_ide/src/type_hierarchy.rs b/crates/ty_ide/src/type_hierarchy.rs new file mode 100644 index 0000000000000..db0d6afa242a9 --- /dev/null +++ b/crates/ty_ide/src/type_hierarchy.rs @@ -0,0 +1,729 @@ +use crate::Db; +use crate::goto::find_goto_target; +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_python_ast::name::Name; +use ruff_text_size::{TextRange, TextSize}; +use ty_python_semantic::SemanticModel; +use ty_python_semantic::TypeHierarchyClass; +use ty_python_semantic::types::Type; + +/// Represents a type hierarchy item returned by the LSP type hierarchy requests. +#[derive(Debug, Clone)] +pub struct TypeHierarchyItem { + /// The name of the type (e.g., `MyClass`). + pub name: Name, + /// The fully-qualified name or detail string (e.g., `mymodule.MyClass`). + pub detail: Option, + /// The file containing the type definition. + pub file: File, + /// The range covering the full class definition. + pub full_range: TextRange, + /// The range of the class name (for selection/focus). + pub selection_range: TextRange, +} + +/// Prepare the type hierarchy at a given position. +/// +/// Returns `None` if the position is not on a class definition or class reference. +pub fn prepare_type_hierarchy( + db: &dyn Db, + file: File, + offset: TextSize, +) -> Option { + let module = parsed_module(db, file).load(db); + let model = SemanticModel::new(db, file); + let goto_target = find_goto_target(&model, &module, offset)?; + let ty = goto_target.inferred_type(&model)?; + + let hierarchy_class = ty_python_semantic::type_hierarchy_prepare(db, ty)?; + Some(type_hierarchy_class_to_item(db, hierarchy_class)) +} + +/// Get the supertypes (base classes) of a type hierarchy item. +pub fn type_hierarchy_supertypes( + db: &dyn Db, + file: File, + offset: TextSize, +) -> Vec { + let Some(ty) = resolve_type_at(db, file, offset) else { + return vec![]; + }; + ty_python_semantic::type_hierarchy_supertypes(db, ty) + .into_iter() + .map(|c| type_hierarchy_class_to_item(db, c)) + .collect() +} + +/// Get the subtypes (derived classes) of a type hierarchy item. +pub fn type_hierarchy_subtypes( + db: &dyn Db, + file: File, + offset: TextSize, +) -> Vec { + let Some(ty) = resolve_type_at(db, file, offset) else { + return vec![]; + }; + ty_python_semantic::type_hierarchy_subtypes(db, ty) + .into_iter() + .map(|c| type_hierarchy_class_to_item(db, c)) + .collect() +} + +/// Returns the type of the symbol under the cursor at `offset` in `file`. +/// +/// If a symbol could not be found at the given offset or its type could +/// not be inferred, `None` is returned. +fn resolve_type_at(db: &dyn Db, file: File, offset: TextSize) -> Option> { + let module = parsed_module(db, file).load(db); + let model = SemanticModel::new(db, file); + + let goto_target = find_goto_target(&model, &module, offset)?; + goto_target.inferred_type(&model) +} + +fn type_hierarchy_class_to_item(db: &dyn Db, class: TypeHierarchyClass) -> TypeHierarchyItem { + let detail = ty_module_resolver::file_to_module(db, class.file) + .map(|module| module.name(db).to_string()); + + TypeHierarchyItem { + name: class.name, + detail, + file: class.file, + full_range: class.full_range, + selection_range: class.selection_range, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{CursorTest, cursor_test}; + + #[test] + fn prepare_type_hierarchy_on_class_def() { + let test = cursor_test( + r#" + class MyClass: + pass + "#, + ); + + let item = test.prepare().unwrap(); + insta::assert_snapshot!(snapshot(&test.db, &[item]), @"/main.py:7:14 MyClass :: main"); + } + + #[test] + fn prepare_type_hierarchy_on_class_usage() { + let test = cursor_test( + r#" + class MyClass: + pass + + x = MyClass() + "#, + ); + + let item = test.prepare().unwrap(); + insta::assert_snapshot!(snapshot(&test.db, &[item]), @"/main.py:7:14 MyClass :: main"); + } + + #[test] + fn prepare_type_hierarchy_on_non_class() { + let test = cursor_test( + r#" + x = 42 + "#, + ); + + assert!(test.prepare().is_none()); + } + + #[test] + fn supertypes_simple_inheritance() { + let test = cursor_test( + r#" + class Base: + pass + + class Derived(Base): + pass + "#, + ); + + let supertypes = test.supertypes(); + insta::assert_snapshot!(snapshot(&test.db, &supertypes), @"/main.py:7:11 Base :: main"); + } + + #[test] + fn supertypes_multiple_inheritance() { + let test = cursor_test( + r#" + class A: + pass + + class B: + pass + + class C(A, B): + pass + "#, + ); + + let mut supertypes = test.supertypes(); + supertypes.sort_by(|a, b| a.name.cmp(&b.name)); + insta::assert_snapshot!(snapshot(&test.db, &supertypes), @r" + /main.py:7:8 A :: main + /main.py:26:27 B :: main + "); + } + + #[test] + fn supertypes_generic_base() { + let test = cursor_test( + r#" + from typing import Generic, TypeVar + + T = TypeVar("T") + + class Base(Generic[T]): + pass + + class Derived(Base[int]): + pass + "#, + ); + + let supertypes = test.supertypes(); + insta::assert_snapshot!(snapshot(&test.db, &supertypes), @"/main.py:62:66 Base :: main"); + } + + #[test] + fn supertypes_implicit_object() { + let test = cursor_test( + r#" + class MyClass: + pass + "#, + ); + + let supertypes = test.supertypes(); + insta::assert_snapshot!( + snapshot(&test.db, &supertypes), + @"vendored://stdlib/builtins.pyi:3608:3614 object :: builtins", + ); + } + + #[test] + fn subtypes_simple() { + let test = cursor_test( + r#" + class Base: + pass + + class Derived1(Base): + pass + + class Derived2(Base): + pass + "#, + ); + + let mut subtypes = test.subtypes(); + subtypes.sort_by(|a, b| a.name.cmp(&b.name)); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @r" + /main.py:29:37 Derived1 :: main + /main.py:61:69 Derived2 :: main + "); + } + + #[test] + fn subtypes_of_object_includes_implicit() { + let test = cursor_test( + r#" + x: type[object] + + class ImplicitChild: + pass + + class ExplicitChild(object): + pass + "#, + ); + + // `object` has hundreds of subtypes across typeshed, + // so we check for specific items rather than snapshotting. + let subtypes = test.subtypes(); + let names: Vec<_> = subtypes.iter().map(|s| s.name.as_str()).collect(); + assert!(names.contains(&"ImplicitChild")); + assert!(names.contains(&"ExplicitChild")); + } + + #[test] + fn subtypes_version_conditional() { + let test = cursor_test( + r#" + import sys + + class Base: + pass + + if sys.version_info >= (3, 5): + class ChildOld(Base): + pass + + if sys.version_info >= (3, 999): + class ChildFuture(Base): + pass + "#, + ); + + // As of 2026-02-25, the default Python version is 3.14, so + // `ChildOld` should be a subtype of `Base`, but `ChildFuture` + // should not. + let subtypes = test.subtypes(); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @"/main.py:76:84 ChildOld :: main"); + } + + /// This is a regression test for a case where we would emit + /// duplicate `MyEventType` subtypes of `str` because of the + /// conditional definition based on Python version. Moreover, since + /// our test's default Python version is newer than 3.11 and since + /// we're asking for the *direct* subtypes of `str`, it follows + /// that we shouldn't see `MyEventType` at all. That's because in + /// the 3.11+ case, it inherit from `StrEnum` and not from `str` + /// directly. + /// + /// So this test actually covers two different bugs: one for + /// duplicates and another for not properly evaluating reachability + /// constraints. + #[test] + fn subtypes_str_conditional_no_duplicates() { + let test = cursor_test( + r#" + import sys + + if sys.version_info >= (3, 11): + from enum import StrEnum + else: + from enum import Enum + + if sys.version_info >= (3, 11): + class MyEventType(StrEnum): + Activate = "36" + ButtonPress = "4" + else: + class MyEventType(str, Enum): + Activate = "36" + ButtonPress = "4" + + x: str = "foo" + "#, + ); + + let subtypes = test.subtypes(); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @r" + vendored://stdlib/email/headerregistry.pyi:703:713 BaseHeader :: email.headerregistry + vendored://stdlib/enum.pyi:18342:18349 StrEnum :: enum + vendored://stdlib/pdb.pyi:38460:38465 _rstr :: pdb + vendored://stdlib/xxlimited.pyi:113:116 Str :: xxlimited + "); + } + + /// Like `subtypes_str_conditional_no_duplicates`, but we make + /// both branches inherit directly from `str`. We should get back + /// `MyEventTypeA` and not `MyEventTypeB`. + #[test] + fn subtypes_str_conditional_one_direct_subtype() { + let test = cursor_test( + r#" + import sys + from enum import Enum + + if sys.version_info >= (3, 11): + class MyEventTypeA(str, Enum): + Activate = "36" + ButtonPress = "4" + else: + class MyEventTypeB(str, Enum): + Activate = "36" + ButtonPress = "4" + + x: str = "foo" + "#, + ); + + let subtypes = test.subtypes(); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @r" + vendored://stdlib/email/headerregistry.pyi:703:713 BaseHeader :: email.headerregistry + vendored://stdlib/enum.pyi:18342:18349 StrEnum :: enum + /main.py:77:89 MyEventTypeA :: main + vendored://stdlib/pdb.pyi:38460:38465 _rstr :: pdb + vendored://stdlib/xxlimited.pyi:113:116 Str :: xxlimited + "); + } + + /// Dynamic classes created via `type()` can be prepared for the + /// type hierarchy. The selection range highlights the variable + /// name, not the `type()` call. + #[test] + fn dynamic_class_prepare_and_supertypes_variable_definition() { + let test = cursor_test( + r#" + class Base: + pass + + Dynamic = type("Dynamic", (Base,), {}) + "#, + ); + + let item = test.prepare().unwrap(); + insta::assert_snapshot!(snapshot(&test.db, &[item]), @"/main.py:23:30 Dynamic :: main"); + + let supertypes = test.supertypes(); + insta::assert_snapshot!(snapshot(&test.db, &supertypes), @"/main.py:7:11 Base :: main"); + } + + /// Like `dynamic_class_prepare_and_supertypes_variable_definition`, but + /// uses an inline `type` call and demonstrates the limitation in the + /// current implementation (as of 2026-02-25). + #[test] + fn dynamic_class_prepare_and_supertypes_inline() { + // This is "fine," but the offsets returned + // for `Dynamic` as a supertype will result + // in subsequent requests showing the type + // hierarchy for `type` instead of `Dynamic`. + let test = cursor_test( + r#" + class Base: + pass + + class Super(type("Dynamic", (Base,), {})): pass + "#, + ); + let item = test.prepare().unwrap(); + insta::assert_snapshot!(snapshot(&test.db, &[item]), @"/main.py:29:34 Super :: main"); + let supertypes = test.supertypes(); + insta::assert_snapshot!(snapshot(&test.db, &supertypes), @"/main.py:35:63 Dynamic :: main"); + + // We emulate that subsequent request here. This is a + // limitation of our current type hierarchy implementation. I + // think ideally we'd recognize the `type(...)` idiom and "see + // through" it. But what if the user actually wants the type + // hierarchy for `type`? Maybe we should only recognize the + // idiom when the cursor is on the `"Dynamic"` string literal. + // ---AG + let test = cursor_test( + r#" + class Base: + pass + + class Super(type("Dynamic", (Base,), {})): pass + "#, + ); + let item = test.prepare().unwrap(); + insta::assert_snapshot!( + snapshot(&test.db, &[item]), + @"vendored://stdlib/builtins.pyi:8615:8619 type :: builtins", + ); + let supertypes = test.supertypes(); + insta::assert_snapshot!( + snapshot(&test.db, &supertypes), + @"vendored://stdlib/builtins.pyi:3608:3614 object :: builtins", + ); + } + + /// Dynamic classes created via `type()` are not found as subtypes + /// because they don't create class scopes. + #[test] + fn dynamic_class_subtypes_of_class_definition() { + let test = cursor_test( + r#" + class Base: + pass + + Dynamic = type("Dynamic", (Base,), {}) + "#, + ); + + assert!(test.subtypes().is_empty()); + } + + #[test] + fn dynamic_class_subtypes_of_dynamic() { + let test = cursor_test( + r#" + Dynamic = type("Dynamic", (object,), {}) + + class Child(Dynamic): pass + "#, + ); + + let subtypes = test.subtypes(); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @"/main.py:49:54 Child :: main"); + } + + /// Like `dynamic_class_prepare_and_supertypes_variable_definition`, + /// but for named tuples. + #[test] + fn namedtuple_prepare_and_supertypes_variable_definition() { + let test = cursor_test( + r#" + from collections import namedtuple + + Dynamic = namedtuple("Dynamic", ['x', 'y']) + "#, + ); + + let item = test.prepare().unwrap(); + insta::assert_snapshot!(snapshot(&test.db, &[item]), @"/main.py:37:44 Dynamic :: main"); + + let supertypes = test.supertypes(); + insta::assert_snapshot!( + snapshot(&test.db, &supertypes), + @"vendored://stdlib/builtins.pyi:101715:101720 tuple :: builtins", + ); + } + + /// Like `dynamic_class_prepare_and_supertypes_inline`, but + /// for named tuples. + #[test] + fn namedtuple_prepare_and_supertypes_inline() { + let test = cursor_test( + r#" + from collections import namedtuple + + class Dynamic(namedtuple("Dynamic", ['x', 'y'])): pass + "#, + ); + let item = test.prepare().unwrap(); + insta::assert_snapshot!(snapshot(&test.db, &[item]), @"/main.py:43:50 Dynamic :: main"); + let supertypes = test.supertypes(); + insta::assert_snapshot!( + snapshot(&test.db, &supertypes), + @"/main.py:51:84 Dynamic :: main", + ); + + // This fails for a different reason than + // `dynamic_class_prepare_and_supertypes_inline` in the + // `type` case. Specifically, `namedtuple` is defined + // as a function, which our current implementation doesn't + // recognize as returning a class. So the prepare request + // doesn't return any items. + let test = cursor_test( + r#" + from collections import namedtuple + + class Dynamic(namedtuple("Dynamic", ['x', 'y'])): pass + "#, + ); + assert!(test.prepare().is_none()); + } + + #[test] + fn namedtuple_subtypes_of_namedtuple() { + let test = cursor_test( + r#" + from collections import namedtuple + + Parent = namedtuple('Parent', ['x', 'y']) + class Child(Parent): pass + "#, + ); + + let subtypes = test.subtypes(); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @"/main.py:85:90 Child :: main"); + } + + /// Named tuples created via `namedtuple()` are not found as subtypes + /// because they don't create class scopes. Typeshed classes that + /// inherit from `tuple` (which are defined as regular class + /// statements) are still found. + #[test] + fn namedtuple_subtypes_of_tuple() { + let test = cursor_test( + r#" + from collections import namedtuple + + MyTuple = namedtuple('MyTuple', ['x', 'y']) + tuple + "#, + ); + + let subtypes = test.subtypes(); + let names: Vec<_> = subtypes.iter().map(|s| s.name.as_str()).collect(); + // `MyTuple` is not found because `namedtuple()` doesn't create a class scope. + assert!(!names.contains(&"MyTuple")); + // But regular class definitions that inherit from `tuple` are found. + assert!(names.contains(&"struct_time")); + } + + /// Re-exports via assignment are not found as subtypes because + /// we only look at class scopes. + /// + /// We could look for these, but when AG tried it, it made perf in + /// some cases quite slow and produced a lot of false positives. + #[test] + fn subtypes_reexport_first_party() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +class Base: + pass +"#, + ) + .source( + "_impl.py", + r#" +from main import Base + +class _Internal(Base): + pass +"#, + ) + .source( + "public.py", + r#" +from main import Base +from _impl import _Internal + +Public = _Internal +"#, + ) + .build(); + + let subtypes = test.subtypes(); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @"/_impl.py:30:39 _Internal :: _impl"); + } + + /// This is like `subtypes_reexport_first_party`, but results in + /// not discovering any subtypes because the re-export isn't + /// discovered. And the original class definition is private. + #[test] + fn subtypes_reexport_third_party() { + let test = CursorTest::builder() + .with_site_packages() + .source("main.py", "bytes") + .site_packages( + "thirdparty/__init__.py", + r#" + from thirdparty._internal import _bytes_internal + BytesPublic = _bytes_internal + "#, + ) + .site_packages( + "thirdparty/_internal.py", + "class _bytes_internal(bytes): pass", + ) + .build(); + + assert!(test.subtypes().is_empty()); + } + + /// This tests that we filter out some subtypes in a way that's consistent + /// with what we do for auto-import. Specifically, subtypes from + /// non-first-party tests or private modules. + #[test] + fn third_party_filtering() { + let test = CursorTest::builder() + .with_site_packages() + .source("main.py", "bytes") + .source("foo.py", "class MyBytes(bytes): pass") + .site_packages("thirdparty/__init__.py", "class OtherBytes1(bytes): pass") + .site_packages( + "thirdparty/_test/__init__.py", + "class OtherBytes2(bytes): pass", + ) + .site_packages( + "thirdparty/_tests/__init__.py", + "class OtherBytes3(bytes): pass", + ) + .site_packages( + "thirdparty/_testing/__init__.py", + "class OtherBytes4(bytes): pass", + ) + .site_packages( + "thirdparty/_foo/__init__.py", + "class OtherBytes5(bytes): pass", + ) + .build(); + + // We should only see our own subtype and the only third-party + // subtype that isn't treated as private. + let subtypes = test.subtypes(); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @r" + /src/foo.py:6:13 MyBytes :: foo + /site-packages/thirdparty/__init__.py:6:17 OtherBytes1 :: thirdparty + "); + } + + /// This tests that we don't currently respect `__all__` when returning + /// subtypes. + #[test] + fn subtypes_all_not_respected() { + let test = CursorTest::builder() + .with_site_packages() + .source("main.py", "bytes") + .source("foo.py", "class MyBytes(bytes): pass") + .site_packages( + "thirdparty/__init__.py", + r#" + class OtherBytes1(bytes): pass + class OtherBytes2(bytes): pass + __all__ = ['OtherBytes1'] + "#, + ) + .build(); + + // I think ideally we wouldn't include `OtherBytes2` here. + // Note that pylance doesn't seem to respect `__all__` in + // this case either. + let subtypes = test.subtypes(); + insta::assert_snapshot!(snapshot(&test.db, &subtypes), @r" + /src/foo.py:6:13 MyBytes :: foo + /site-packages/thirdparty/__init__.py:7:18 OtherBytes1 :: thirdparty + /site-packages/thirdparty/__init__.py:38:49 OtherBytes2 :: thirdparty + "); + } + + fn snapshot(db: &dyn Db, items: &[TypeHierarchyItem]) -> String { + items + .iter() + .map(|item| { + let mut string = format!( + "{path}:{start}:{end} {name}", + path = item.file.path(db), + start = item.selection_range.start().to_usize(), + end = item.selection_range.end().to_usize(), + name = item.name, + ); + if let Some(ref detail) = item.detail { + string = format!("{string} :: {detail}"); + } + string + }) + .collect::>() + .join("\n") + } + + impl CursorTest { + fn prepare(&self) -> Option { + prepare_type_hierarchy(&self.db, self.cursor.file, self.cursor.offset) + } + + fn supertypes(&self) -> Vec { + let Some(item) = self.prepare() else { + return vec![]; + }; + type_hierarchy_supertypes(&self.db, item.file, item.selection_range.start()) + } + + fn subtypes(&self) -> Vec { + let Some(item) = self.prepare() else { + return vec![]; + }; + type_hierarchy_subtypes(&self.db, item.file, item.selection_range.start()) + } + } +} diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 64adc46b5a119..afb5f55f6480f 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -26,9 +26,10 @@ pub use ty_site_packages::{ SitePackagesPaths, SysPrefixPathOrigin, }; pub use types::ide_support::{ - ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_bin_op, - definitions_for_imported_symbol, definitions_for_name, definitions_for_unary_op, - map_stub_definition, + ImportAliasResolution, ResolvedDefinition, TypeHierarchyClass, definitions_for_attribute, + definitions_for_bin_op, definitions_for_imported_symbol, definitions_for_name, + definitions_for_unary_op, map_stub_definition, type_hierarchy_prepare, type_hierarchy_subtypes, + type_hierarchy_supertypes, }; pub use types::{DisplaySettings, TypeQualifiers}; diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index e49d3188104ec..82b470ff8c44a 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -473,7 +473,7 @@ impl<'db> SemanticIndex<'db> { .map(|node_ref| self.expect_single_definition(node_ref)) } - fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool { + pub(crate) fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool { self.parent_scope_id(scope_id) .is_none_or(|parent_scope_id| { if !self.is_scope_reachable(db, parent_scope_id) { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 900cc0a9bcf13..5a545310b3893 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -6,16 +6,19 @@ use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::DefinitionKind; use crate::semantic_index::{attribute_scopes, global_scope, semantic_index, use_def_map}; use crate::types::call::{CallArguments, CallError, MatchedArgument}; +use crate::types::class::{DynamicClassAnchor, DynamicNamedTupleAnchor}; use crate::types::constraints::ConstraintSetBuilder; use crate::types::signatures::{ParameterKind, Signature}; use crate::types::{ - CallDunderError, CallableTypes, ClassBase, ClassLiteral, ClassType, KnownUnion, Type, - TypeContext, UnionType, + CallDunderError, CallableTypes, ClassBase, ClassLiteral, ClassType, KnownClass, KnownUnion, + Type, TypeContext, UnionType, }; use crate::{Db, DisplaySettings, HasDefinition, HasType, SemanticModel}; use itertools::Either; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; +use ruff_db::source::source_text; +use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; @@ -1575,3 +1578,240 @@ mod resolve_definition { Ok(component) } } + +/// Information about a class in the type hierarchy. +#[derive(Debug, Clone)] +pub struct TypeHierarchyClass { + /// The name of the class. + pub name: Name, + /// The file containing the class definition. + pub file: ruff_db::files::File, + /// The range covering the full class definition header. + pub full_range: TextRange, + /// The range of the class name (for selection/focus). + pub selection_range: TextRange, +} + +/// Return a type hierarchy item for the class type given. +/// +/// When the type given doesn't correspond to a class literal, then this always +/// returns `None`. +/// +/// This is meant to be used to "prepare" for a subtype or supertype request. +/// That is, this effectively validates whether the given type can be used in +/// subsequent requests for supertypes or subtypes. +pub fn type_hierarchy_prepare(db: &dyn Db, ty: Type<'_>) -> Option { + let class_literal = extract_class_literal(db, ty)?; + Some(class_literal_to_hierarchy_info(db, class_literal)) +} + +/// Get the direct base classes for the class type given. +/// +/// When the type given doesn't correspond to a class literal, then this always +/// returns an empty sequence. +/// +/// This includes `object` when the given class has no direct base classes. +pub fn type_hierarchy_supertypes(db: &dyn Db, ty: Type<'_>) -> Vec { + let Some(class_literal) = extract_class_literal(db, ty) else { + return vec![]; + }; + if class_literal.is_known(db, KnownClass::Object) { + return vec![]; + } + + let mut supertypes: Vec = class_literal + .explicit_bases(db) + .into_iter() + .filter_map(|base| extract_class_literal(db, base)) + .map(|class_literal| class_literal_to_hierarchy_info(db, class_literal)) + .collect(); + // Every class implicitly inherits from `object` when no explicit + // bases are declared. + if supertypes.is_empty() { + supertypes.push(class_literal_to_hierarchy_info( + db, + ClassLiteral::object(db), + )); + } + supertypes +} + +/// Get the direct subtypes of the class given. +/// +/// When the type given doesn't correspond to a class literal, then this always +/// returns an empty sequence. +/// +/// Note that this scans all modules in `db` to find classes that directly +/// inherit from the given class. This could be quite expensive in large +/// projects. +pub fn type_hierarchy_subtypes(db: &dyn Db, ty: Type<'_>) -> Vec { + let Some(target_class) = extract_class_literal(db, ty) else { + return vec![]; + }; + let target_name = target_class.name(db); + let target_is_object = target_class.is_known(db, KnownClass::Object); + let mut subtypes = vec![]; + + // Scan all modules in the workspace + for module in ty_module_resolver::all_modules(db) { + let Some(file) = module.file(db) else { + continue; + }; + + // Note that this will always consider namespace + // packages to be "not firsty party." This isn't + // necessarily correct, and we can probably improve + // on this in response to user feedback. + let is_non_first_party = module.search_path(db).is_none_or(|sp| !sp.is_first_party()); + let name = module.name(db); + // Filter out non-first-party modules that are conventionally + // regarded as private or tests. + if is_non_first_party && (name.is_private() || name.is_test_module()) { + continue; + } + + // Skip files that don't contain the class name. This avoids expensive + // semantic analysis for files that can't possibly contain a subclass + // of the target. We can't do this when looking for subtypes of + // `object` since `object` can be implicit. + if !target_is_object && !source_text(db, file).contains(target_name.as_str()) { + continue; + } + + let index = semantic_index(db, file); + for scope_id in index.scope_ids() { + let scope = scope_id.node(db); + let Some(class_node) = scope.as_class() else { + continue; + }; + + let def = index.expect_single_definition(class_node); + if !matches!(def.kind(db), DefinitionKind::Class(_)) { + continue; + } + + let file_scope_id = scope_id.file_scope_id(db); + if !index.is_scope_reachable(db, file_scope_id) { + continue; + } + + let ty = crate::types::binding_type(db, def); + let Some(class_ty) = extract_class_literal(db, ty) else { + continue; + }; + + let bases = class_ty.explicit_bases(db); + let is_subtype = if target_is_object + && bases.is_empty() + && !class_ty.is_known(db, KnownClass::Object) + { + true + } else { + bases.iter().any(|base| { + extract_class_literal(db, *base) + .is_some_and(|base_literal| base_literal == target_class) + }) + }; + if is_subtype { + subtypes.push(class_literal_to_hierarchy_info(db, class_ty)); + } + } + } + subtypes +} + +/// Extract a `ClassLiteral` from a `Type`, handling various type forms. +fn extract_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { + match ty { + Type::ClassLiteral(class_literal) => Some(class_literal), + Type::SubclassOf(subclass_of) => { + let inner = subclass_of.subclass_of(); + match inner { + crate::types::SubclassOfInner::Class(class_type) => { + Some(class_type.class_literal(db)) + } + crate::types::SubclassOfInner::Dynamic(_) + | crate::types::SubclassOfInner::TypeVar(_) => None, + } + } + Type::GenericAlias(generic_alias) => Some(ClassLiteral::Static(generic_alias.origin(db))), + Type::NominalInstance(instance) => Some(instance.class(db).class_literal(db)), + Type::Union(union) => union + .elements(db) + .iter() + .find_map(|elem| extract_class_literal(db, *elem)), + + _ => None, + } +} + +/// Convert a `ClassLiteral` to `TypeHierarchyClass` info. +/// +/// For the most part, this is about extracting the right +/// text ranges. +fn class_literal_to_hierarchy_info( + db: &dyn Db, + class_literal: ClassLiteral<'_>, +) -> TypeHierarchyClass { + let name = class_literal.name(db).clone(); + let file = class_literal.file(db); + + let (full_range, selection_range) = match class_literal { + ClassLiteral::Static(static_class) => { + let parsed = parsed_module(db, file).load(db); + let header_range = static_class.header_range(db); + let body_scope = static_class.body_scope(db); + + let selection_range = body_scope + .node(db) + .as_class() + .map(|c| c.node(&parsed)) + .map(|class_def| class_def.name.range()) + .unwrap_or(header_range); + (header_range, selection_range) + } + // For the dynamic cases, we special case a variable definition + // like this: + // + // Dynamic = type("Dynamic", (object,), {}) + // + // In this case, the range for the element we return will correspond to + // the left hand side of the variable assignment. This works better as + // an "anchor" point because it avoids ambiguity with asking for the + // type hierarchy of `type` itself. + // + // If there is not a variable definition, then we fall back to the + // class definition's "header" range, which will be the `type` (or + // `namedtuple`) call. Subsequent type hierarchy requests will then + // (likely incorrectly) return the type hierarchy for `type` itself. + ClassLiteral::Dynamic(dynamic_class) => { + if let DynamicClassAnchor::Definition(definition) = dynamic_class.anchor(db) { + let parsed = parsed_module(db, file).load(db); + let kind = definition.kind(db); + (kind.full_range(&parsed), kind.target_range(&parsed)) + } else { + let header_range = dynamic_class.header_range(db); + (header_range, header_range) + } + } + ClassLiteral::DynamicNamedTuple(namedtuple) => { + if let DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } + | DynamicNamedTupleAnchor::TypingDefinition(definition) = namedtuple.anchor(db) + { + let parsed = parsed_module(db, file).load(db); + let kind = definition.kind(db); + (kind.full_range(&parsed), kind.target_range(&parsed)) + } else { + let header_range = namedtuple.header_range(db); + (header_range, header_range) + } + } + }; + + TypeHierarchyClass { + name, + file, + full_range, + selection_range, + } +} From 43ca0820e2018ab01e506979802eb81b45a7145b Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 25 Feb 2026 14:37:01 -0500 Subject: [PATCH 116/261] [ty] Add routine for mapping from system path to vendored path In some cases, the LSP client will send us file paths corresponding to a vendored file in typeshed. We really want to treat this as a `VendoredPath` and not a `SystemPath`. In particular, if we treat it as the latter, we can end up with two different interned `File` values for the same file. And this leads to disastrous things (like definitions no longer being equivalent). --- crates/ty_ide/src/lib.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index ca04a62f5fb89..7bca400d2f927 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -56,7 +56,7 @@ pub use workspace_symbols::{WorkspaceSymbolInfo, workspace_symbols}; use ruff_db::{ files::{File, FileRange}, - system::SystemPathBuf, + system::{SystemPath, SystemPathBuf}, vendored::VendoredPath, }; use ruff_text_size::{Ranged, TextRange}; @@ -351,6 +351,25 @@ pub fn cached_vendored_root(db: &dyn ty_python_semantic::Db) -> Option( + cached_vendored_root: &SystemPath, + absolute_path: &'a SystemPath, +) -> Option<&'a VendoredPath> { + let rel_path = absolute_path.strip_prefix(cached_vendored_root).ok()?; + Some(VendoredPath::new(rel_path.as_str())) +} + #[cfg(test)] mod tests { use camino::Utf8Component; From fa951ea5f46a2961e8c28f15070f7d34672ebd88 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 25 Feb 2026 14:38:43 -0500 Subject: [PATCH 117/261] [ty] Wire up the type hierarchy implementation with the LSP I think the main interesting piece here is mapping file URLs back to their appropriate vendored or system file path type. Otherwise, this mostly just wraps the API exposed in `ty_ide`. We also add a few "sanity check" end-to-end tests, but leave the bulk of the testing responsibility to `ty_ide`. --- crates/ty_server/src/capabilities.rs | 1 + crates/ty_server/src/server/api.rs | 16 ++ crates/ty_server/src/server/api/requests.rs | 4 + .../src/server/api/requests/type_hierarchy.rs | 229 ++++++++++++++++++ crates/ty_server/tests/e2e/main.rs | 1 + .../e2e__initialize__initialization.snap | 1 + ...ialize__initialization_with_workspace.snap | 1 + crates/ty_server/tests/e2e/type_hierarchy.rs | 201 +++++++++++++++ 8 files changed, 454 insertions(+) create mode 100644 crates/ty_server/src/server/api/requests/type_hierarchy.rs create mode 100644 crates/ty_server/tests/e2e/type_hierarchy.rs diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index 21a3c13474ebf..46f0a5f3761ee 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -469,6 +469,7 @@ pub(crate) fn server_capabilities( }), ..Default::default() }), + type_hierarchy_provider: Some(OneOf::Left(true)), ..Default::default() } } diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index 0d35ff30cc406..f3193e34fe247 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -108,6 +108,22 @@ pub(super) fn request(req: server::Request) -> Task { >( req, BackgroundSchedule::Worker ), + requests::PrepareTypeHierarchyRequestHandler::METHOD => background_document_request_task::< + requests::PrepareTypeHierarchyRequestHandler, + >( + req, BackgroundSchedule::Worker + ), + requests::TypeHierarchySupertypesRequestHandler::METHOD => { + background_request_task::( + req, + BackgroundSchedule::Worker, + ) + } + requests::TypeHierarchySubtypesRequestHandler::METHOD => background_request_task::< + requests::TypeHierarchySubtypesRequestHandler, + >( + req, BackgroundSchedule::Worker + ), lsp_types::request::Shutdown::METHOD => sync_request_task::(req), method => { diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs index 8b1cafa2dd197..7b97a63ba8136 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -18,6 +18,7 @@ mod semantic_tokens; mod semantic_tokens_range; mod shutdown; mod signature_help; +mod type_hierarchy; mod workspace_diagnostic; mod workspace_symbols; @@ -41,6 +42,9 @@ pub(super) use semantic_tokens::SemanticTokensRequestHandler; pub(super) use semantic_tokens_range::SemanticTokensRangeRequestHandler; pub(super) use shutdown::ShutdownHandler; pub(super) use signature_help::SignatureHelpRequestHandler; +pub(super) use type_hierarchy::PrepareTypeHierarchyRequestHandler; +pub(super) use type_hierarchy::TypeHierarchySubtypesRequestHandler; +pub(super) use type_hierarchy::TypeHierarchySupertypesRequestHandler; pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler; pub(super) use workspace_symbols::WorkspaceSymbolRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/type_hierarchy.rs b/crates/ty_server/src/server/api/requests/type_hierarchy.rs new file mode 100644 index 0000000000000..c9f17af105535 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/type_hierarchy.rs @@ -0,0 +1,229 @@ +use std::borrow::Cow; + +use lsp_types::request::{TypeHierarchyPrepare, TypeHierarchySubtypes, TypeHierarchySupertypes}; +use lsp_types::{ + SymbolKind, TypeHierarchyItem, TypeHierarchyPrepareParams, TypeHierarchySubtypesParams, + TypeHierarchySupertypesParams, Url, +}; +use ruff_db::files::{File, system_path_to_file, vendored_path_to_file}; +use ruff_db::system::SystemPathBuf; +use ruff_text_size::TextSize; +use ty_project::ProjectDatabase; + +use crate::PositionEncoding; +use crate::document::{PositionExt, ToRangeExt}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, BackgroundRequestHandler, RequestHandler, + RetriableRequestHandler, +}; +use crate::session::DocumentSnapshot; +use crate::session::SessionSnapshot; +use crate::session::client::Client; +use crate::system::file_to_url; + +/// Handles a `textDocument/prepareTypeHierarchy` request. +/// +/// This is the "initial" request for identifying the type hierarchy of a +/// symbol in a document. In particular, it identifies the actual target based +/// on the current cursor position and returns a single "type hierarchy item" +/// corresponding to that symbol. +/// +/// From there, a subsequent request can be made by the client to get either +/// the subtypes or supertypes of that symbol. +pub(crate) struct PrepareTypeHierarchyRequestHandler; + +impl RequestHandler for PrepareTypeHierarchyRequestHandler { + type RequestType = TypeHierarchyPrepare; +} + +impl BackgroundDocumentRequestHandler for PrepareTypeHierarchyRequestHandler { + fn document_url(params: &TypeHierarchyPrepareParams) -> Cow<'_, Url> { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: &DocumentSnapshot, + _client: &Client, + params: TypeHierarchyPrepareParams, + ) -> crate::server::Result>> { + if snapshot + .workspace_settings() + .is_language_services_disabled() + { + return Ok(None); + } + + let Some(file) = snapshot.to_notebook_or_file(db) else { + return Ok(None); + }; + + let Some(offset) = params.text_document_position_params.position.to_text_size( + db, + file, + snapshot.url(), + snapshot.encoding(), + ) else { + return Ok(None); + }; + + let Some(item) = ty_ide::prepare_type_hierarchy(db, file, offset) else { + return Ok(None); + }; + + let Some(lsp_item) = convert_to_lsp_item(db, item, snapshot.encoding()) else { + return Ok(None); + }; + + Ok(Some(vec![lsp_item])) + } +} + +impl RetriableRequestHandler for PrepareTypeHierarchyRequestHandler {} + +/// Handles a `typeHierarchy/supertypes` request. +/// +/// Note that this implements the `BackgroundRequestHandler` because the +/// request might be for a symbol in a document that is not open in the current +/// session. +pub(crate) struct TypeHierarchySupertypesRequestHandler; + +impl RequestHandler for TypeHierarchySupertypesRequestHandler { + type RequestType = TypeHierarchySupertypes; +} + +impl BackgroundRequestHandler for TypeHierarchySupertypesRequestHandler { + fn run( + snapshot: &SessionSnapshot, + _client: &Client, + params: TypeHierarchySupertypesParams, + ) -> crate::server::Result>> { + Ok(hierarchy_handler( + snapshot, + ¶ms.item, + ty_ide::type_hierarchy_supertypes, + )) + } +} + +impl RetriableRequestHandler for TypeHierarchySupertypesRequestHandler {} + +/// Handles a `typeHierarchy/subtypes` request. +/// +/// Note that this implements the `BackgroundRequestHandler` because the +/// request might be for a symbol in a document that is not open in the current +/// session. +pub(crate) struct TypeHierarchySubtypesRequestHandler; + +impl RequestHandler for TypeHierarchySubtypesRequestHandler { + type RequestType = TypeHierarchySubtypes; +} + +impl BackgroundRequestHandler for TypeHierarchySubtypesRequestHandler { + fn run( + snapshot: &SessionSnapshot, + _client: &Client, + params: TypeHierarchySubtypesParams, + ) -> crate::server::Result>> { + Ok(hierarchy_handler( + snapshot, + ¶ms.item, + ty_ide::type_hierarchy_subtypes, + )) + } +} + +impl RetriableRequestHandler for TypeHierarchySubtypesRequestHandler {} + +/// The subtype and supertype implementation. +/// +/// `hierarchy_types` should be either `ty_ide::type_hierarchy_subtypes` +/// or `ty_ide::type_hierarchy_supertypes`. +fn hierarchy_handler( + snapshot: &SessionSnapshot, + requested_item: &TypeHierarchyItem, + hierarchy_types: fn(&dyn ty_project::Db, File, TextSize) -> Vec, +) -> Option> { + let encoding = snapshot.position_encoding(); + + // We don't actually know which project the request + // came from, so just look for results across all + // projects. + let mut items = vec![]; + for db in snapshot.projects() { + let Some((file, offset)) = resolve_item_location(db, requested_item, encoding) else { + continue; + }; + items.extend( + hierarchy_types(db, file, offset) + .into_iter() + .filter_map(|item| convert_to_lsp_item(db, item, encoding)), + ); + } + if items.is_empty() { None } else { Some(items) } +} + +/// Attempts to resolve the location in the provided +/// type hierarchy item into `ty_ide` types. This includes +/// mapping system paths back into their proper vendored +/// path types (if applicable). +fn resolve_item_location( + db: &ProjectDatabase, + item: &TypeHierarchyItem, + encoding: PositionEncoding, +) -> Option<(File, TextSize)> { + let system_path = SystemPathBuf::from_path_buf(item.uri.to_file_path().ok()?).ok()?; + + let file = if let Some(ref vendored_root) = ty_ide::cached_vendored_root(db) + && let Some(vendored_path) = ty_ide::map_system_to_vendored(vendored_root, &system_path) + { + match vendored_path_to_file(db, vendored_path) { + Ok(file) => file, + Err(err) => { + tracing::warn!( + "Could not resolve type hierarchy item location \ + for vendored file path `{vendored_path}`: {err}" + ); + return None; + } + } + } else { + match system_path_to_file(db, &system_path) { + Ok(file) => file, + Err(err) => { + tracing::warn!( + "Could not resolve type hierarchy item location \ + for system file path `{system_path}`: {err}" + ); + return None; + } + } + }; + + let offset = item + .selection_range + .start + .to_text_size(db, file, &item.uri, encoding)?; + Some((file, offset)) +} + +fn convert_to_lsp_item( + db: &ProjectDatabase, + item: ty_ide::TypeHierarchyItem, + encoding: PositionEncoding, +) -> Option { + let uri = file_to_url(db, item.file)?; + let full_range = item.full_range.to_lsp_range(db, item.file, encoding)?; + let selection_range = item.selection_range.to_lsp_range(db, item.file, encoding)?; + + Some(TypeHierarchyItem { + name: item.name.into(), + kind: SymbolKind::CLASS, + tags: None, + detail: item.detail, + uri, + range: full_range.local_range(), + selection_range: selection_range.local_range(), + data: None, + }) +} diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index aba9fa60f889e..86a79237f8688 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -40,6 +40,7 @@ mod pull_diagnostics; mod rename; mod semantic_tokens; mod signature_help; +mod type_hierarchy; mod workspace_folders; use std::collections::{BTreeMap, HashMap, VecDeque}; diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap index 63fba38b3c9cd..70da6fccdfb27 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap @@ -94,6 +94,7 @@ expression: initialization_result "range": true, "full": true }, + "typeHierarchyProvider": true, "inlayHintProvider": {}, "diagnosticProvider": { "identifier": "ty", diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap index 63fba38b3c9cd..70da6fccdfb27 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap @@ -94,6 +94,7 @@ expression: initialization_result "range": true, "full": true }, + "typeHierarchyProvider": true, "inlayHintProvider": {}, "diagnosticProvider": { "identifier": "ty", diff --git a/crates/ty_server/tests/e2e/type_hierarchy.rs b/crates/ty_server/tests/e2e/type_hierarchy.rs new file mode 100644 index 0000000000000..df91f22228c8a --- /dev/null +++ b/crates/ty_server/tests/e2e/type_hierarchy.rs @@ -0,0 +1,201 @@ +use lsp_types::request::{TypeHierarchyPrepare, TypeHierarchySubtypes, TypeHierarchySupertypes}; +use lsp_types::{ + PartialResultParams, Position, TextDocumentIdentifier, TextDocumentPositionParams, + TypeHierarchyPrepareParams, TypeHierarchySubtypesParams, TypeHierarchySupertypesParams, + WorkDoneProgressParams, +}; + +use crate::TestServerBuilder; + +#[test] +fn simple_supertypes() -> anyhow::Result<()> { + let content = r#"class Base: + pass + +class Derived(Base): + pass +"#; + + let mut server = TestServerBuilder::new()? + .enable_pull_diagnostics(true) + .with_file("foo.py", content)? + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document("foo.py", content, 1); + + // Prepare on `Derived` + let items = prepare(&mut server, "foo.py", Position::new(3, 8)).unwrap(); + assert_eq!(items[0].name, "Derived"); + + // Get supertypes of `Derived` + let bases = supertypes(&mut server, items[0].clone()).unwrap(); + assert_eq!(bases.len(), 1); + assert_eq!(bases[0].name, "Base"); + + Ok(()) +} + +/// Tests that we can query for multiple subtypes. +#[test] +fn simple_subtypes() -> anyhow::Result<()> { + let content = r#"class Base: + pass + +class Child1(Base): + pass + +class Child2(Base): + pass +"#; + + let mut server = TestServerBuilder::new()? + .enable_pull_diagnostics(true) + .with_file("foo.py", content)? + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document("foo.py", content, 1); + + let items = prepare(&mut server, "foo.py", Position::new(0, 8)).unwrap(); + assert_eq!(items[0].name, "Base"); + + let children = subtypes(&mut server, items[0].clone()).unwrap(); + assert_eq!(children.len(), 2); + + let names: Vec<_> = children.iter().map(|c| c.name.as_str()).collect(); + assert!(names.contains(&"Child1")); + assert!(names.contains(&"Child2")); + + Ok(()) +} + +/// This tests that we can start at a class and then issue +/// repeated supertype requests until we reach the top of +/// the class hierarchy (`object`). +#[test] +fn chained_hierarchy() -> anyhow::Result<()> { + let content = r#"class Grandparent: + pass + +class Parent(Grandparent): + pass + +class Child(Parent): + pass +"#; + + let mut server = TestServerBuilder::new()? + .enable_pull_diagnostics(true) + .with_file("foo.py", content)? + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document("foo.py", content, 1); + + // Start at Child, and walk up the class hierarchy. + let items = prepare(&mut server, "foo.py", Position::new(6, 8)).unwrap(); + assert_eq!(items[0].name, "Child"); + + let parents = supertypes(&mut server, items[0].clone()).unwrap(); + assert_eq!(parents.len(), 1); + assert_eq!(parents[0].name, "Parent"); + + let grandparents = supertypes(&mut server, parents[0].clone()).unwrap(); + assert_eq!(grandparents.len(), 1); + assert_eq!(grandparents[0].name, "Grandparent"); + + let top = supertypes(&mut server, grandparents[0].clone()).unwrap(); + assert_eq!(top.len(), 1); + assert_eq!(top[0].name, "object"); + + // `object` has no supertypes + let beyond = supertypes(&mut server, top[0].clone()); + assert!(beyond.is_none()); + + Ok(()) +} + +/// Tests that the type hierarchy works for types defined in vendored +/// (typeshed) files, where the document URI provided by the client +/// points to a cached system path that must be mapped back to a +/// vendored path. +/// +/// This is a regression test that the initial type hierarchy +/// implementation failed. In particular, the system path to a +/// vendored file provided by the client wasn't being mapped back to +/// a `VendoredPath`, and this in turn ultimately resulted in two +/// different interned `File` values for the same typeshed file. This +/// led to downstream issues related to type equality. +#[test] +fn vendored_supertypes() -> anyhow::Result<()> { + let content = "from enum import StrEnum"; + let mut server = TestServerBuilder::new()? + .enable_pull_diagnostics(true) + .with_file("foo.py", content)? + .build() + .wait_until_workspaces_are_initialized(); + server.open_text_document("foo.py", content, 1); + + let items = prepare(&mut server, "foo.py", Position::new(0, 20)).unwrap(); + assert_eq!(items[0].name, "StrEnum"); + + // Note that we don't actually assert anything about the + // URI in `items[0]`. This test matches the actual flow + // that failed, which is the actually important thing to + // test. + let bases = supertypes(&mut server, items[0].clone()).unwrap(); + let names: Vec<_> = bases.iter().map(|b| b.name.as_str()).collect(); + assert!(names.contains(&"str")); + assert!(names.contains(&"ReprEnum")); + + // Follow `ReprEnum` to its supertypes — another vendored round-trip + // that exercises resolving the vendored URI back to a `File`. + let repr_enum = bases.iter().find(|b| b.name == "ReprEnum").unwrap(); + let repr_enum_bases = supertypes(&mut server, repr_enum.clone()).unwrap(); + let names: Vec<_> = repr_enum_bases.iter().map(|b| b.name.as_str()).collect(); + assert!(names.contains(&"Enum")); + + Ok(()) +} + +/// Sends a `textDocument/prepareTypeHierarchy` request. +fn prepare( + server: &mut crate::TestServer, + path: impl AsRef, + position: Position, +) -> Option> { + server.send_request_await::(TypeHierarchyPrepareParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { + uri: server.file_uri(path), + }, + position, + }, + work_done_progress_params: WorkDoneProgressParams::default(), + }) +} + +/// Sends a `typeHierarchy/supertypes` request. +fn supertypes( + server: &mut crate::TestServer, + item: lsp_types::TypeHierarchyItem, +) -> Option> { + server.send_request_await::(TypeHierarchySupertypesParams { + item, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }) +} + +/// Sends a `typeHierarchy/subtypes` request. +fn subtypes( + server: &mut crate::TestServer, + item: lsp_types::TypeHierarchyItem, +) -> Option> { + server.send_request_await::(TypeHierarchySubtypesParams { + item, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }) +} From e45f226cdb10ed7633eacadc3647f5afb4950f54 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 27 Feb 2026 07:42:57 -0500 Subject: [PATCH 118/261] [ty] Move the type hierarchy request handlers to individual modules --- crates/ty_server/src/server/api.rs | 1 + crates/ty_server/src/server/api/requests.rs | 24 +- .../api/requests/prepare_type_hierarchy.rs | 73 ++++++ .../src/server/api/requests/type_hierarchy.rs | 229 ------------------ .../api/requests/type_hierarchy_subtypes.rs | 36 +++ .../api/requests/type_hierarchy_supertypes.rs | 36 +++ .../src/server/api/type_hierarchy.rs | 103 ++++++++ 7 files changed, 269 insertions(+), 233 deletions(-) create mode 100644 crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs delete mode 100644 crates/ty_server/src/server/api/requests/type_hierarchy.rs create mode 100644 crates/ty_server/src/server/api/requests/type_hierarchy_subtypes.rs create mode 100644 crates/ty_server/src/server/api/requests/type_hierarchy_supertypes.rs create mode 100644 crates/ty_server/src/server/api/type_hierarchy.rs diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index f3193e34fe247..74637f3775a0c 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -13,6 +13,7 @@ mod requests; mod semantic_tokens; mod symbols; mod traits; +mod type_hierarchy; use self::traits::{NotificationHandler, RequestHandler}; use super::{Result, schedule::BackgroundSchedule}; diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs index 7b97a63ba8136..ceae1f3a677a9 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -1,3 +1,17 @@ +/*! +This module provides the trait implementations necessary to implement each of +the LSP request handlers. + +Every request handler should live in its own module, with a module name +matching the LSP protocol request name as closely as possible. This should be +done even when there is tight coupling between multiple request handlers (like +type hierarchy) to make it easy to continue to find the right handler given +knowledge about the request name. + +If request handlers need shared helper functions, they can go in a sibling +module. For example, see `super::type_hierarchy`. +*/ + mod code_action; mod completion; mod diagnostic; @@ -11,6 +25,7 @@ mod goto_type_definition; mod hover; mod inlay_hints; mod prepare_rename; +mod prepare_type_hierarchy; mod references; mod rename; mod selection_range; @@ -18,7 +33,8 @@ mod semantic_tokens; mod semantic_tokens_range; mod shutdown; mod signature_help; -mod type_hierarchy; +mod type_hierarchy_subtypes; +mod type_hierarchy_supertypes; mod workspace_diagnostic; mod workspace_symbols; @@ -35,6 +51,7 @@ pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; pub(super) use hover::HoverRequestHandler; pub(super) use inlay_hints::InlayHintRequestHandler; pub(super) use prepare_rename::PrepareRenameRequestHandler; +pub(super) use prepare_type_hierarchy::PrepareTypeHierarchyRequestHandler; pub(super) use references::ReferencesRequestHandler; pub(super) use rename::RenameRequestHandler; pub(super) use selection_range::SelectionRangeRequestHandler; @@ -42,9 +59,8 @@ pub(super) use semantic_tokens::SemanticTokensRequestHandler; pub(super) use semantic_tokens_range::SemanticTokensRangeRequestHandler; pub(super) use shutdown::ShutdownHandler; pub(super) use signature_help::SignatureHelpRequestHandler; -pub(super) use type_hierarchy::PrepareTypeHierarchyRequestHandler; -pub(super) use type_hierarchy::TypeHierarchySubtypesRequestHandler; -pub(super) use type_hierarchy::TypeHierarchySupertypesRequestHandler; +pub(super) use type_hierarchy_subtypes::TypeHierarchySubtypesRequestHandler; +pub(super) use type_hierarchy_supertypes::TypeHierarchySupertypesRequestHandler; pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler; pub(super) use workspace_symbols::WorkspaceSymbolRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs b/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs new file mode 100644 index 0000000000000..a680f8a95886f --- /dev/null +++ b/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs @@ -0,0 +1,73 @@ +use std::borrow::Cow; + +use lsp_types::request::TypeHierarchyPrepare; +use lsp_types::{TypeHierarchyItem, TypeHierarchyPrepareParams, Url}; +use ty_project::ProjectDatabase; + +use crate::document::PositionExt; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::server::api::type_hierarchy::convert_to_lsp_item; +use crate::session::DocumentSnapshot; +use crate::session::client::Client; + +/// Handles a `textDocument/prepareTypeHierarchy` request. +/// +/// This is the "initial" request for identifying the type hierarchy of a +/// symbol in a document. In particular, it identifies the actual target based +/// on the current cursor position and returns a single "type hierarchy item" +/// corresponding to that symbol. +/// +/// From there, a subsequent request can be made by the client to get either +/// the subtypes or supertypes of that symbol. +pub(crate) struct PrepareTypeHierarchyRequestHandler; + +impl RequestHandler for PrepareTypeHierarchyRequestHandler { + type RequestType = TypeHierarchyPrepare; +} + +impl BackgroundDocumentRequestHandler for PrepareTypeHierarchyRequestHandler { + fn document_url(params: &TypeHierarchyPrepareParams) -> Cow<'_, Url> { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: &DocumentSnapshot, + _client: &Client, + params: TypeHierarchyPrepareParams, + ) -> crate::server::Result>> { + if snapshot + .workspace_settings() + .is_language_services_disabled() + { + return Ok(None); + } + + let Some(file) = snapshot.to_notebook_or_file(db) else { + return Ok(None); + }; + + let Some(offset) = params.text_document_position_params.position.to_text_size( + db, + file, + snapshot.url(), + snapshot.encoding(), + ) else { + return Ok(None); + }; + + let Some(item) = ty_ide::prepare_type_hierarchy(db, file, offset) else { + return Ok(None); + }; + + let Some(lsp_item) = convert_to_lsp_item(db, item, snapshot.encoding()) else { + return Ok(None); + }; + + Ok(Some(vec![lsp_item])) + } +} + +impl RetriableRequestHandler for PrepareTypeHierarchyRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/type_hierarchy.rs b/crates/ty_server/src/server/api/requests/type_hierarchy.rs deleted file mode 100644 index c9f17af105535..0000000000000 --- a/crates/ty_server/src/server/api/requests/type_hierarchy.rs +++ /dev/null @@ -1,229 +0,0 @@ -use std::borrow::Cow; - -use lsp_types::request::{TypeHierarchyPrepare, TypeHierarchySubtypes, TypeHierarchySupertypes}; -use lsp_types::{ - SymbolKind, TypeHierarchyItem, TypeHierarchyPrepareParams, TypeHierarchySubtypesParams, - TypeHierarchySupertypesParams, Url, -}; -use ruff_db::files::{File, system_path_to_file, vendored_path_to_file}; -use ruff_db::system::SystemPathBuf; -use ruff_text_size::TextSize; -use ty_project::ProjectDatabase; - -use crate::PositionEncoding; -use crate::document::{PositionExt, ToRangeExt}; -use crate::server::api::traits::{ - BackgroundDocumentRequestHandler, BackgroundRequestHandler, RequestHandler, - RetriableRequestHandler, -}; -use crate::session::DocumentSnapshot; -use crate::session::SessionSnapshot; -use crate::session::client::Client; -use crate::system::file_to_url; - -/// Handles a `textDocument/prepareTypeHierarchy` request. -/// -/// This is the "initial" request for identifying the type hierarchy of a -/// symbol in a document. In particular, it identifies the actual target based -/// on the current cursor position and returns a single "type hierarchy item" -/// corresponding to that symbol. -/// -/// From there, a subsequent request can be made by the client to get either -/// the subtypes or supertypes of that symbol. -pub(crate) struct PrepareTypeHierarchyRequestHandler; - -impl RequestHandler for PrepareTypeHierarchyRequestHandler { - type RequestType = TypeHierarchyPrepare; -} - -impl BackgroundDocumentRequestHandler for PrepareTypeHierarchyRequestHandler { - fn document_url(params: &TypeHierarchyPrepareParams) -> Cow<'_, Url> { - Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) - } - - fn run_with_snapshot( - db: &ProjectDatabase, - snapshot: &DocumentSnapshot, - _client: &Client, - params: TypeHierarchyPrepareParams, - ) -> crate::server::Result>> { - if snapshot - .workspace_settings() - .is_language_services_disabled() - { - return Ok(None); - } - - let Some(file) = snapshot.to_notebook_or_file(db) else { - return Ok(None); - }; - - let Some(offset) = params.text_document_position_params.position.to_text_size( - db, - file, - snapshot.url(), - snapshot.encoding(), - ) else { - return Ok(None); - }; - - let Some(item) = ty_ide::prepare_type_hierarchy(db, file, offset) else { - return Ok(None); - }; - - let Some(lsp_item) = convert_to_lsp_item(db, item, snapshot.encoding()) else { - return Ok(None); - }; - - Ok(Some(vec![lsp_item])) - } -} - -impl RetriableRequestHandler for PrepareTypeHierarchyRequestHandler {} - -/// Handles a `typeHierarchy/supertypes` request. -/// -/// Note that this implements the `BackgroundRequestHandler` because the -/// request might be for a symbol in a document that is not open in the current -/// session. -pub(crate) struct TypeHierarchySupertypesRequestHandler; - -impl RequestHandler for TypeHierarchySupertypesRequestHandler { - type RequestType = TypeHierarchySupertypes; -} - -impl BackgroundRequestHandler for TypeHierarchySupertypesRequestHandler { - fn run( - snapshot: &SessionSnapshot, - _client: &Client, - params: TypeHierarchySupertypesParams, - ) -> crate::server::Result>> { - Ok(hierarchy_handler( - snapshot, - ¶ms.item, - ty_ide::type_hierarchy_supertypes, - )) - } -} - -impl RetriableRequestHandler for TypeHierarchySupertypesRequestHandler {} - -/// Handles a `typeHierarchy/subtypes` request. -/// -/// Note that this implements the `BackgroundRequestHandler` because the -/// request might be for a symbol in a document that is not open in the current -/// session. -pub(crate) struct TypeHierarchySubtypesRequestHandler; - -impl RequestHandler for TypeHierarchySubtypesRequestHandler { - type RequestType = TypeHierarchySubtypes; -} - -impl BackgroundRequestHandler for TypeHierarchySubtypesRequestHandler { - fn run( - snapshot: &SessionSnapshot, - _client: &Client, - params: TypeHierarchySubtypesParams, - ) -> crate::server::Result>> { - Ok(hierarchy_handler( - snapshot, - ¶ms.item, - ty_ide::type_hierarchy_subtypes, - )) - } -} - -impl RetriableRequestHandler for TypeHierarchySubtypesRequestHandler {} - -/// The subtype and supertype implementation. -/// -/// `hierarchy_types` should be either `ty_ide::type_hierarchy_subtypes` -/// or `ty_ide::type_hierarchy_supertypes`. -fn hierarchy_handler( - snapshot: &SessionSnapshot, - requested_item: &TypeHierarchyItem, - hierarchy_types: fn(&dyn ty_project::Db, File, TextSize) -> Vec, -) -> Option> { - let encoding = snapshot.position_encoding(); - - // We don't actually know which project the request - // came from, so just look for results across all - // projects. - let mut items = vec![]; - for db in snapshot.projects() { - let Some((file, offset)) = resolve_item_location(db, requested_item, encoding) else { - continue; - }; - items.extend( - hierarchy_types(db, file, offset) - .into_iter() - .filter_map(|item| convert_to_lsp_item(db, item, encoding)), - ); - } - if items.is_empty() { None } else { Some(items) } -} - -/// Attempts to resolve the location in the provided -/// type hierarchy item into `ty_ide` types. This includes -/// mapping system paths back into their proper vendored -/// path types (if applicable). -fn resolve_item_location( - db: &ProjectDatabase, - item: &TypeHierarchyItem, - encoding: PositionEncoding, -) -> Option<(File, TextSize)> { - let system_path = SystemPathBuf::from_path_buf(item.uri.to_file_path().ok()?).ok()?; - - let file = if let Some(ref vendored_root) = ty_ide::cached_vendored_root(db) - && let Some(vendored_path) = ty_ide::map_system_to_vendored(vendored_root, &system_path) - { - match vendored_path_to_file(db, vendored_path) { - Ok(file) => file, - Err(err) => { - tracing::warn!( - "Could not resolve type hierarchy item location \ - for vendored file path `{vendored_path}`: {err}" - ); - return None; - } - } - } else { - match system_path_to_file(db, &system_path) { - Ok(file) => file, - Err(err) => { - tracing::warn!( - "Could not resolve type hierarchy item location \ - for system file path `{system_path}`: {err}" - ); - return None; - } - } - }; - - let offset = item - .selection_range - .start - .to_text_size(db, file, &item.uri, encoding)?; - Some((file, offset)) -} - -fn convert_to_lsp_item( - db: &ProjectDatabase, - item: ty_ide::TypeHierarchyItem, - encoding: PositionEncoding, -) -> Option { - let uri = file_to_url(db, item.file)?; - let full_range = item.full_range.to_lsp_range(db, item.file, encoding)?; - let selection_range = item.selection_range.to_lsp_range(db, item.file, encoding)?; - - Some(TypeHierarchyItem { - name: item.name.into(), - kind: SymbolKind::CLASS, - tags: None, - detail: item.detail, - uri, - range: full_range.local_range(), - selection_range: selection_range.local_range(), - data: None, - }) -} diff --git a/crates/ty_server/src/server/api/requests/type_hierarchy_subtypes.rs b/crates/ty_server/src/server/api/requests/type_hierarchy_subtypes.rs new file mode 100644 index 0000000000000..9411a0350bcc9 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/type_hierarchy_subtypes.rs @@ -0,0 +1,36 @@ +use lsp_types::request::TypeHierarchySubtypes; +use lsp_types::{TypeHierarchyItem, TypeHierarchySubtypesParams}; + +use crate::server::api::traits::{ + BackgroundRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::server::api::type_hierarchy::hierarchy_handler; +use crate::session::SessionSnapshot; +use crate::session::client::Client; + +/// Handles a `typeHierarchy/subtypes` request. +/// +/// Note that this implements the `BackgroundRequestHandler` because the +/// request might be for a symbol in a document that is not open in the current +/// session. +pub(crate) struct TypeHierarchySubtypesRequestHandler; + +impl RequestHandler for TypeHierarchySubtypesRequestHandler { + type RequestType = TypeHierarchySubtypes; +} + +impl BackgroundRequestHandler for TypeHierarchySubtypesRequestHandler { + fn run( + snapshot: &SessionSnapshot, + _client: &Client, + params: TypeHierarchySubtypesParams, + ) -> crate::server::Result>> { + Ok(hierarchy_handler( + snapshot, + ¶ms.item, + ty_ide::type_hierarchy_subtypes, + )) + } +} + +impl RetriableRequestHandler for TypeHierarchySubtypesRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/type_hierarchy_supertypes.rs b/crates/ty_server/src/server/api/requests/type_hierarchy_supertypes.rs new file mode 100644 index 0000000000000..663a3649172ec --- /dev/null +++ b/crates/ty_server/src/server/api/requests/type_hierarchy_supertypes.rs @@ -0,0 +1,36 @@ +use lsp_types::request::TypeHierarchySupertypes; +use lsp_types::{TypeHierarchyItem, TypeHierarchySupertypesParams}; + +use crate::server::api::traits::{ + BackgroundRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::server::api::type_hierarchy::hierarchy_handler; +use crate::session::SessionSnapshot; +use crate::session::client::Client; + +/// Handles a `typeHierarchy/supertypes` request. +/// +/// Note that this implements the `BackgroundRequestHandler` because the +/// request might be for a symbol in a document that is not open in the current +/// session. +pub(crate) struct TypeHierarchySupertypesRequestHandler; + +impl RequestHandler for TypeHierarchySupertypesRequestHandler { + type RequestType = TypeHierarchySupertypes; +} + +impl BackgroundRequestHandler for TypeHierarchySupertypesRequestHandler { + fn run( + snapshot: &SessionSnapshot, + _client: &Client, + params: TypeHierarchySupertypesParams, + ) -> crate::server::Result>> { + Ok(hierarchy_handler( + snapshot, + ¶ms.item, + ty_ide::type_hierarchy_supertypes, + )) + } +} + +impl RetriableRequestHandler for TypeHierarchySupertypesRequestHandler {} diff --git a/crates/ty_server/src/server/api/type_hierarchy.rs b/crates/ty_server/src/server/api/type_hierarchy.rs new file mode 100644 index 0000000000000..982eecba887b8 --- /dev/null +++ b/crates/ty_server/src/server/api/type_hierarchy.rs @@ -0,0 +1,103 @@ +use lsp_types::{SymbolKind, TypeHierarchyItem}; +use ruff_db::files::{File, system_path_to_file, vendored_path_to_file}; +use ruff_db::system::SystemPathBuf; +use ruff_text_size::TextSize; +use ty_project::ProjectDatabase; + +use crate::PositionEncoding; +use crate::document::{PositionExt, ToRangeExt}; +use crate::session::SessionSnapshot; +use crate::system::file_to_url; + +/// The subtype and supertype implementation. +/// +/// `hierarchy_types` should be either `ty_ide::type_hierarchy_subtypes` +/// or `ty_ide::type_hierarchy_supertypes`. +pub(crate) fn hierarchy_handler( + snapshot: &SessionSnapshot, + requested_item: &TypeHierarchyItem, + hierarchy_types: fn(&dyn ty_project::Db, File, TextSize) -> Vec, +) -> Option> { + let encoding = snapshot.position_encoding(); + + // We don't actually know which project the request + // came from, so just look for results across all + // projects. + let mut items = vec![]; + for db in snapshot.projects() { + let Some((file, offset)) = resolve_item_location(db, requested_item, encoding) else { + continue; + }; + items.extend( + hierarchy_types(db, file, offset) + .into_iter() + .filter_map(|item| convert_to_lsp_item(db, item, encoding)), + ); + } + if items.is_empty() { None } else { Some(items) } +} + +/// Attempts to resolve the location in the provided +/// type hierarchy item into `ty_ide` types. This includes +/// mapping system paths back into their proper vendored +/// path types (if applicable). +fn resolve_item_location( + db: &ProjectDatabase, + item: &TypeHierarchyItem, + encoding: PositionEncoding, +) -> Option<(File, TextSize)> { + let system_path = SystemPathBuf::from_path_buf(item.uri.to_file_path().ok()?).ok()?; + + let file = if let Some(ref vendored_root) = ty_ide::cached_vendored_root(db) + && let Some(vendored_path) = ty_ide::map_system_to_vendored(vendored_root, &system_path) + { + match vendored_path_to_file(db, vendored_path) { + Ok(file) => file, + Err(err) => { + tracing::warn!( + "Could not resolve type hierarchy item location \ + for vendored file path `{vendored_path}`: {err}" + ); + return None; + } + } + } else { + match system_path_to_file(db, &system_path) { + Ok(file) => file, + Err(err) => { + tracing::warn!( + "Could not resolve type hierarchy item location \ + for system file path `{system_path}`: {err}" + ); + return None; + } + } + }; + + let offset = item + .selection_range + .start + .to_text_size(db, file, &item.uri, encoding)?; + Some((file, offset)) +} + +pub(crate) fn convert_to_lsp_item( + db: &ProjectDatabase, + item: ty_ide::TypeHierarchyItem, + encoding: PositionEncoding, +) -> Option { + let uri = file_to_url(db, item.file)?; + let full_range = item.full_range.to_lsp_range(db, item.file, encoding)?; + let selection_range = item.selection_range.to_lsp_range(db, item.file, encoding)?; + + Some(TypeHierarchyItem { + name: item.name.into(), + kind: SymbolKind::CLASS, + tags: None, + detail: item.detail, + uri, + range: full_range.local_range(), + selection_range: selection_range.local_range(), + data: None, + }) +} From 619a5ac18e8408ccdaf350a9f1dadb8a795e4f86 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 27 Feb 2026 13:30:08 +0000 Subject: [PATCH 119/261] [ty] Detect invalid uses of `@final` on non-methods (#23604) ## Summary This fixes the last remaining conformance failure on https://github.com/python/typing/blob/main/conformance/tests/qualifiers_final_decorator.py ## Test Plan mdtests --- crates/ty/docs/rules.md | 228 ++++++++++-------- .../resources/mdtest/final.md | 23 ++ .../src/types/diagnostic.rs | 27 +++ .../src/types/infer/builder.rs | 60 +++-- ty.schema.json | 10 + 5 files changed, 232 insertions(+), 116 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 178fb687ba991..759e5754ef232 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -634,13 +634,45 @@ Static analysis tools like ty can't analyze type annotations that contain escape def foo() -> "intt\b": ... ``` +## `final-on-non-method` + + +Default level: error · +Added in 0.0.20 · +Related issues · +View source + + + +**What it does** + +Checks for `@final` decorators applied to non-method functions. + +**Why is this bad?** + +The `@final` decorator is only meaningful on methods and classes. +Applying it to a module-level function or a nested function has no +effect and is likely a mistake. + +**Example** + + +```python +from typing import final + +# Error: @final is not allowed on non-method functions +@final +def my_function() -> int: + return 0 +``` + ## `final-without-value` Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -766,7 +798,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -796,7 +828,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -822,7 +854,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -856,7 +888,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -945,7 +977,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -972,7 +1004,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1000,7 +1032,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1034,7 +1066,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1070,7 +1102,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1094,7 +1126,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1121,7 +1153,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1158,7 +1190,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1190,7 +1222,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1219,7 +1251,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1263,7 +1295,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1305,7 +1337,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1349,7 +1381,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1387,7 +1419,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1466,7 +1498,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1505,7 +1537,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1566,7 +1598,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1601,7 +1633,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1629,7 +1661,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1663,7 +1695,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1770,7 +1802,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1824,7 +1856,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1854,7 +1886,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1904,7 +1936,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1930,7 +1962,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1961,7 +1993,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1995,7 +2027,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2044,7 +2076,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2073,7 +2105,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2169,7 +2201,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2215,7 +2247,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2242,7 +2274,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2289,7 +2321,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2319,7 +2351,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2349,7 +2381,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2383,7 +2415,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2417,7 +2449,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2448,7 +2480,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2495,7 +2527,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2527,7 +2559,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2562,7 +2594,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2593,7 +2625,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2648,7 +2680,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2691,7 +2723,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2716,7 +2748,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2749,7 +2781,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2778,7 +2810,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2804,7 +2836,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2828,7 +2860,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2861,7 +2893,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2894,7 +2926,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2921,7 +2953,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2948,7 +2980,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2976,7 +3008,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3008,7 +3040,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3045,7 +3077,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3109,7 +3141,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3136,7 +3168,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3168,7 +3200,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3198,7 +3230,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3227,7 +3259,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3261,7 +3293,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3288,7 +3320,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3316,7 +3348,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3362,7 +3394,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3386,7 +3418,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3413,7 +3445,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3441,7 +3473,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3499,7 +3531,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3524,7 +3556,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3549,7 +3581,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3588,7 +3620,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3625,7 +3657,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3666,7 +3698,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3767,7 +3799,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3830,7 +3862,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md index 1b6081908f339..78149aabcd1bc 100644 --- a/crates/ty_python_semantic/resources/mdtest/final.md +++ b/crates/ty_python_semantic/resources/mdtest/final.md @@ -428,6 +428,29 @@ class D(B): # error: [subclass-of-final-class] def method(self): ... # error: [override-of-final-variable] ``` +## `@final` cannot be applied to non-method functions + +The `@final` decorator is only valid on methods and classes. Using it on a module-level or nested +function is an error. + +```py +from typing import final + +@final # error: [final-on-non-method] "`@final` cannot be applied to non-method function `func1`" +def func1(): ... + +# Nested function decorated with `@final` is also invalid +def outer(): + @final # error: [final-on-non-method] + def inner(): ... + +# A function nested inside a method is also not a method +class F: + def method(self): + @final # error: [final-on-non-method] + def not_a_method(): ... +``` + ## An `@final` method is overridden by an implicit instance attribute ```py diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 6fcfec2fcc7d4..0485f2fad1d40 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -119,6 +119,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&OVERRIDE_OF_FINAL_METHOD); registry.register_lint(&OVERRIDE_OF_FINAL_VARIABLE); registry.register_lint(&INEFFECTIVE_FINAL); + registry.register_lint(&FINAL_ON_NON_METHOD); registry.register_lint(&FINAL_WITHOUT_VALUE); registry.register_lint(&ABSTRACT_METHOD_IN_FINAL_CLASS); registry.register_lint(&CALL_ABSTRACT_METHOD); @@ -2162,6 +2163,32 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for `@final` decorators applied to non-method functions. + /// + /// ## Why is this bad? + /// The `@final` decorator is only meaningful on methods and classes. + /// Applying it to a module-level function or a nested function has no + /// effect and is likely a mistake. + /// + /// ## Example + /// + /// ```python + /// from typing import final + /// + /// # Error: @final is not allowed on non-method functions + /// @final + /// def my_function() -> int: + /// return 0 + /// ``` + pub(crate) static FINAL_ON_NON_METHOD = { + summary: "detects `@final` applied to non-method functions", + status: LintStatus::stable("0.0.20"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for `Final` symbols that are declared without a value and are never diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 45fd4c31202ad..a9ce47c3049cf 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -74,21 +74,21 @@ use crate::types::diagnostic::{ self, ABSTRACT_METHOD_IN_FINAL_CLASS, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DATACLASS_FIELD_ORDER, DIVISION_BY_ZERO, DUPLICATE_BASE, DUPLICATE_KW_ONLY, - FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, - INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DATACLASS, - INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, INVALID_KEY, - INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, - INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, - INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, - INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, - INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, MISSING_ARGUMENT, - NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, PARAMETER_ALREADY_ASSIGNED, - POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, - SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, - UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, - hint_if_stdlib_attribute_exists_on_other_versions, + FINAL_ON_NON_METHOD, FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, + INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, + INVALID_DATACLASS, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, + INVALID_KEY, INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, + INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, + INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, + INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, + INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, + INVALID_TYPE_VARIABLE_DEFAULT, INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, + IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, + PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, + POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, + TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, + UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, + UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, report_call_to_abstract_method, report_cannot_delete_typed_dict_key, @@ -3187,6 +3187,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut function_decorators = FunctionDecorators::empty(); let mut deprecated = None; let mut dataclass_transformer_params = None; + let mut final_decorator = None; for decorator in decorator_list { let decorator_type = self.infer_decorator(decorator); @@ -3195,14 +3196,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { function_decorators |= decorator_function_decorator; match decorator_type { - Type::FunctionLiteral(function) => { - if let Some(KnownFunction::NoTypeCheck) = function.known(self.db()) { + Type::FunctionLiteral(function) => match function.known(self.db()) { + Some(KnownFunction::NoTypeCheck) => { // If the function is decorated with the `no_type_check` decorator, // we need to suppress any errors that come after the decorators. self.context.set_in_no_type_check(InNoTypeCheck::Yes); continue; } - } + Some(KnownFunction::Final) => { + final_decorator = Some(decorator); + continue; + } + _ => {} + }, Type::KnownInstance(KnownInstanceType::Deprecated(deprecated_inst)) => { deprecated = Some(deprecated_inst); } @@ -3218,6 +3224,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { decorator_types_and_nodes.push((decorator_type, decorator)); } + // Check for `@final` applied to non-method functions. + // `@final` is only meaningful on methods and classes. + if let Some(final_decorator) = final_decorator + && !self + .index + .scope(self.scope().file_scope_id(self.db())) + .kind() + .is_class() + && let Some(builder) = self + .context + .report_lint(&FINAL_ON_NON_METHOD, final_decorator) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@final` cannot be applied to non-method function `{name}`", + )); + diagnostic.info("`@final` is only meaningful on methods and classes"); + } + let has_defaults = parameters .iter_non_variadic_params() .any(|param| param.default.is_some()); diff --git a/ty.schema.json b/ty.schema.json index cd9f861c8c7a3..ff954b6d68d2a 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -555,6 +555,16 @@ } ] }, + "final-on-non-method": { + "title": "detects `@final` applied to non-method functions", + "description": "## What it does\nChecks for `@final` decorators applied to non-method functions.\n\n## Why is this bad?\nThe `@final` decorator is only meaningful on methods and classes.\nApplying it to a module-level function or a nested function has no\neffect and is likely a mistake.\n\n## Example\n\n```python\nfrom typing import final\n\n# Error: @final is not allowed on non-method functions\n@final\ndef my_function() -> int:\n return 0\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "final-without-value": { "title": "detects `Final` declarations without a value", "description": "## What it does\nChecks for `Final` symbols that are declared without a value and are never\nassigned a value in their scope.\n\n## Why is this bad?\nA `Final` symbol must be initialized with a value at the time of declaration\nor in a subsequent assignment. At module or function scope, the assignment must\noccur in the same scope. In a class body, the assignment may occur in `__init__`.\n\n## Examples\n```python\nfrom typing import Final\n\n# Error: `Final` symbol without a value\nMY_CONSTANT: Final[int]\n\n# OK: `Final` symbol with a value\nMY_CONSTANT: Final[int] = 1\n```", From e30a40e2315da18a8c4db26142e2e1716c741484 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 27 Feb 2026 13:31:35 +0000 Subject: [PATCH 120/261] Update typing conformance suite commit (#23606) Co-authored-by: Claude --- .github/workflows/typing_conformance.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index 97f1c8b576c57..d7aa4a7056926 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -34,7 +34,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 RUST_BACKTRACE: 1 - CONFORMANCE_SUITE_COMMIT: 21b07859158d2d10ed7fe8d9b365412518ed9888 + CONFORMANCE_SUITE_COMMIT: e9fccc9dbbd8f1e8b24b4f88911c3d3155059e2a PYTHON_VERSION: 3.12 jobs: From 6ad151ac5c024c1d169e8e6cf40d9b0f8a271a8f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 27 Feb 2026 14:52:59 +0000 Subject: [PATCH 121/261] [ty] Recurse into tuples and nested tuples when applying special-cased validation of `isinstance()` and `issubclass()` (#23607) ## Summary Refactor the validation logic for `isinstance()` and `issubclass()` calls to support checking tuples (both literal and non-literal) that contain invalid types like protocol classes, TypedDicts, `typing.Any`, and invalid `UnionType` instances. Previously, validation only worked when these invalid types were passed directly as the second argument. Now we recursively validate each element in tuples, enabling detection of errors in cases like: - `isinstance(obj, (int, SomeProtocol))` - `isinstance(obj, (int, SomeTypedDict))` - `isinstance(obj, (int, typing.Any))` - `isinstance(obj, (int, list[int] | bytes))` This fixes https://github.com/astral-sh/ty/issues/1600 and improves our typing conformance score ## Test Plan mdtests and snapshots extended and updated --- .../resources/mdtest/annotations/any.md | 11 + .../resources/mdtest/call/builtins.md | 8 + .../resources/mdtest/narrow/isinstance.md | 44 +++ .../resources/mdtest/narrow/issubclass.md | 35 ++ .../resources/mdtest/protocols.md | 39 +- ...an_in\342\200\246_(eeef56c0ef87a30b).snap" | 75 ++++ ...an_in\342\200\246_(7bb66a0f412caac1).snap" | 87 ++++- ...rotoco\342\200\246_(98257e7c2300373).snap" | 245 +++++++++++++ .../resources/mdtest/typed_dict.md | 14 + .../ty_python_semantic/src/types/function.rs | 334 ++++++++++-------- 10 files changed, 746 insertions(+), 146 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/any.md b/crates/ty_python_semantic/resources/mdtest/annotations/any.md index eef66d6b74c6b..4ad5749372b8b 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/any.md @@ -185,8 +185,19 @@ And `Any` cannot be used in `isinstance()` checks: isinstance("", Any) ``` +The same applies when `Any` is nested inside a tuple, including non-literal tuples: + +```py +isinstance("", (int, Any)) # error: [invalid-argument-type] +isinstance("", (int, (str, Any))) # error: [invalid-argument-type] +classes = (int, Any) +isinstance("", classes) # error: [invalid-argument-type] +``` + But `issubclass()` checks are fine: ```py issubclass(object, Any) # no error! +issubclass(object, (int, Any)) # no error! +issubclass(object, (int, (str, Any))) # no error! ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 5d783a93d3426..286411aae6ea5 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -164,6 +164,11 @@ isinstance("", t.Callable | t.Deque) # `Any` is valid in `issubclass()` calls but not `isinstance()` calls issubclass(list, t.Any) issubclass(list, t.Any | t.Dict) + +# The same works in tuples +isinstance("", (int, t.Dict)) +isinstance("", (int, t.Callable)) +issubclass(list, (int, t.Any)) ``` But for other special forms that are not permitted as the second argument, we still emit an error: @@ -173,6 +178,9 @@ isinstance("", t.TypeGuard) # error: [invalid-argument-type] isinstance("", t.ClassVar) # error: [invalid-argument-type] isinstance("", t.Final) # error: [invalid-argument-type] isinstance("", t.Any) # error: [invalid-argument-type] + +# The same applies when `Any` is nested inside a tuple +isinstance("", (int, t.Any)) # error: [invalid-argument-type] ``` ## The builtin `NotImplemented` constant is not callable diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index b3fcd35aef69f..b44f28efde8ac 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -128,6 +128,41 @@ def _(x: int | list[int] | bytes): reveal_type(x) # revealed: int | list[int] | bytes ``` +The same validation also applies when an invalid `UnionType` is nested inside a tuple: + +```py +def _(x: int | list[int] | bytes): + # error: [invalid-argument-type] + if isinstance(x, (int, list[int] | bytes)): + reveal_type(x) # revealed: int | list[int] | bytes + else: + reveal_type(x) # revealed: int | list[int] | bytes +``` + +Including nested tuples: + +```py +def _(x: int | list[int] | bytes): + # error: [invalid-argument-type] + if isinstance(x, (int, (str, list[int] | bytes))): + reveal_type(x) # revealed: int | list[int] | bytes + else: + reveal_type(x) # revealed: int | list[int] | bytes +``` + +And non-literal tuples: + +```py +classes = (int, list[int] | bytes) + +def _(x: int | list[int] | bytes): + # error: [invalid-argument-type] + if isinstance(x, classes): + reveal_type(x) # revealed: int | list[int] | bytes + else: + reveal_type(x) # revealed: int | list[int] | bytes +``` + ## PEP-604 unions on Python \<3.10 PEP-604 unions were added in Python 3.10, so attempting to use them on Python 3.9 does not lead to @@ -312,6 +347,15 @@ def _(flag: bool): reveal_type(x) # revealed: Literal[1, "a"] ``` +## Splatted calls with invalid `classinfo` + +Diagnostics are still emitted for invalid `classinfo` types when the arguments are splatted: + +```py +args = (object(), int | list[str]) +isinstance(*args) # error: [invalid-argument-type] +``` + ## Generic aliases are not supported as second argument The `classinfo` argument cannot be a generic alias: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index ff6d116da58b3..6199073bc1a3b 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -181,6 +181,41 @@ def _(x: type[int | list | bytes]): reveal_type(x) # revealed: type[int | list[Unknown] | bytes] ``` +The same validation also applies when an invalid `UnionType` is nested inside a tuple: + +```py +def _(x: type[int | list | bytes]): + # error: [invalid-argument-type] + if issubclass(x, (int, list[int] | bytes)): + reveal_type(x) # revealed: type[int | list[Unknown] | bytes] + else: + reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +``` + +Including nested tuples: + +```py +def _(x: type[int | list | bytes]): + # error: [invalid-argument-type] + if issubclass(x, (int, (str, list[int] | bytes))): + reveal_type(x) # revealed: type[int | list[Unknown] | bytes] + else: + reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +``` + +And non-literal tuples: + +```py +classes = (int, list[int] | bytes) + +def _(x: type[int | list | bytes]): + # error: [invalid-argument-type] + if issubclass(x, classes): + reveal_type(x) # revealed: type[int | list[Unknown] | bytes] + else: + reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +``` + ## PEP-604 unions on Python \<3.10 PEP-604 unions were added in Python 3.10, so attempting to use them on Python 3.9 does not lead to diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index cdc5bfd1da28c..cee9b8eec3c79 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2601,6 +2601,41 @@ def f(arg1: type): reveal_type(arg1) # revealed: type & ~type[OnlyClassmethodMembers] ``` +The same diagnostics are also emitted when protocol classes appear inside a tuple passed as the +second argument to `isinstance()` or `issubclass()`: + +```py +def g(arg: object, arg2: type): + isinstance(arg, (HasX, RuntimeCheckableHasX)) # error: [isinstance-against-protocol] + isinstance(arg, (HasX, int)) # error: [isinstance-against-protocol] + + # error: [isinstance-against-protocol] + # error: [isinstance-against-protocol] + issubclass(arg2, (HasX, RuntimeCheckableHasX)) + + issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol] +``` + +This includes nested tuples: + +```py +def g2(arg: object, arg2: type): + isinstance(arg, (int, (HasX, str))) # error: [isinstance-against-protocol] + + # error: [isinstance-against-protocol] + # error: [isinstance-against-protocol] + issubclass(arg2, (int, (HasX, RuntimeCheckableHasX))) +``` + +This also works when the tuple is not a literal in the source: + +```py +classes = (HasX, int) + +def h(arg: object): + isinstance(arg, classes) # error: [isinstance-against-protocol] +``` + ## Match class patterns and protocols @@ -3049,11 +3084,13 @@ static_assert(not is_disjoint_from(Proto, Nominal)) This snippet caused us to panic on an early version of the implementation for protocols. ```py -from typing import Protocol +from typing import Protocol, runtime_checkable +@runtime_checkable class A(Protocol): def x(self) -> "B | A": ... +@runtime_checkable class B(Protocol): def y(self): ... diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins\342\200\246_-_`classinfo`_is_an_in\342\200\246_(eeef56c0ef87a30b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins\342\200\246_-_`classinfo`_is_an_in\342\200\246_(eeef56c0ef87a30b).snap" index 73ce5e0bd9cd7..a6c462f3f7403 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins\342\200\246_-_`classinfo`_is_an_in\342\200\246_(eeef56c0ef87a30b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins\342\200\246_-_`classinfo`_is_an_in\342\200\246_(eeef56c0ef87a30b).snap" @@ -27,6 +27,26 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md 12 | reveal_type(x) # revealed: int | list[int] | bytes 13 | else: 14 | reveal_type(x) # revealed: int | list[int] | bytes +15 | def _(x: int | list[int] | bytes): +16 | # error: [invalid-argument-type] +17 | if isinstance(x, (int, list[int] | bytes)): +18 | reveal_type(x) # revealed: int | list[int] | bytes +19 | else: +20 | reveal_type(x) # revealed: int | list[int] | bytes +21 | def _(x: int | list[int] | bytes): +22 | # error: [invalid-argument-type] +23 | if isinstance(x, (int, (str, list[int] | bytes))): +24 | reveal_type(x) # revealed: int | list[int] | bytes +25 | else: +26 | reveal_type(x) # revealed: int | list[int] | bytes +27 | classes = (int, list[int] | bytes) +28 | +29 | def _(x: int | list[int] | bytes): +30 | # error: [invalid-argument-type] +31 | if isinstance(x, classes): +32 | reveal_type(x) # revealed: int | list[int] | bytes +33 | else: +34 | reveal_type(x) # revealed: int | list[int] | bytes ``` # Diagnostics @@ -87,3 +107,58 @@ info: Element `` in the union, and 2 more elements, a info: rule `invalid-argument-type` is enabled by default ``` + +``` +error[invalid-argument-type]: Invalid second argument to `isinstance` + --> src/mdtest_snippet.py:17:8 + | +15 | def _(x: int | list[int] | bytes): +16 | # error: [invalid-argument-type] +17 | if isinstance(x, (int, list[int] | bytes)): + | ^^^^^^^^^^^^^^^^^^^^-----------------^^ + | | + | This `UnionType` instance contains non-class elements +18 | reveal_type(x) # revealed: int | list[int] | bytes +19 | else: + | +info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects +info: Element `` in the union is not a class object +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Invalid second argument to `isinstance` + --> src/mdtest_snippet.py:23:8 + | +21 | def _(x: int | list[int] | bytes): +22 | # error: [invalid-argument-type] +23 | if isinstance(x, (int, (str, list[int] | bytes))): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^-----------------^^^ + | | + | This `UnionType` instance contains non-class elements +24 | reveal_type(x) # revealed: int | list[int] | bytes +25 | else: + | +info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects +info: Element `` in the union is not a class object +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Invalid second argument to `isinstance` + --> src/mdtest_snippet.py:31:8 + | +29 | def _(x: int | list[int] | bytes): +30 | # error: [invalid-argument-type] +31 | if isinstance(x, classes): + | ^^^^^^^^^^^^^^^^^^^^^^ +32 | reveal_type(x) # revealed: int | list[int] | bytes +33 | else: + | +info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects +info: Element `` in the union `list[int] | bytes` is not a class object +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub\342\200\246_-_`classinfo`_is_an_in\342\200\246_(7bb66a0f412caac1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub\342\200\246_-_`classinfo`_is_an_in\342\200\246_(7bb66a0f412caac1).snap" index f98df2862d2e1..ebcb7cda0e93f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub\342\200\246_-_`classinfo`_is_an_in\342\200\246_(7bb66a0f412caac1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub\342\200\246_-_`classinfo`_is_an_in\342\200\246_(7bb66a0f412caac1).snap" @@ -13,12 +13,32 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md ## mdtest_snippet.py ``` -1 | def _(x: type[int | list | bytes]): -2 | # error: [invalid-argument-type] -3 | if issubclass(x, int | list[int]): -4 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] -5 | else: -6 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] + 1 | def _(x: type[int | list | bytes]): + 2 | # error: [invalid-argument-type] + 3 | if issubclass(x, int | list[int]): + 4 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] + 5 | else: + 6 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] + 7 | def _(x: type[int | list | bytes]): + 8 | # error: [invalid-argument-type] + 9 | if issubclass(x, (int, list[int] | bytes)): +10 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +11 | else: +12 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +13 | def _(x: type[int | list | bytes]): +14 | # error: [invalid-argument-type] +15 | if issubclass(x, (int, (str, list[int] | bytes))): +16 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +17 | else: +18 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +19 | classes = (int, list[int] | bytes) +20 | +21 | def _(x: type[int | list | bytes]): +22 | # error: [invalid-argument-type] +23 | if issubclass(x, classes): +24 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +25 | else: +26 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] ``` # Diagnostics @@ -41,3 +61,58 @@ info: Element `` in the union is not a class object info: rule `invalid-argument-type` is enabled by default ``` + +``` +error[invalid-argument-type]: Invalid second argument to `issubclass` + --> src/mdtest_snippet.py:9:8 + | + 7 | def _(x: type[int | list | bytes]): + 8 | # error: [invalid-argument-type] + 9 | if issubclass(x, (int, list[int] | bytes)): + | ^^^^^^^^^^^^^^^^^^^^-----------------^^ + | | + | This `UnionType` instance contains non-class elements +10 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +11 | else: + | +info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects +info: Element `` in the union is not a class object +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Invalid second argument to `issubclass` + --> src/mdtest_snippet.py:15:8 + | +13 | def _(x: type[int | list | bytes]): +14 | # error: [invalid-argument-type] +15 | if issubclass(x, (int, (str, list[int] | bytes))): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^-----------------^^^ + | | + | This `UnionType` instance contains non-class elements +16 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +17 | else: + | +info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects +info: Element `` in the union is not a class object +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Invalid second argument to `issubclass` + --> src/mdtest_snippet.py:23:8 + | +21 | def _(x: type[int | list | bytes]): +22 | # error: [invalid-argument-type] +23 | if issubclass(x, classes): + | ^^^^^^^^^^^^^^^^^^^^^^ +24 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] +25 | else: + | +info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects +info: Element `` in the union `list[int] | bytes` is not a class object +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" index b4aab670b07f8..c98c2072ac520 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" @@ -74,6 +74,25 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md 59 | reveal_type(arg1) # revealed: type[OnlyClassmethodMembers] 60 | else: 61 | reveal_type(arg1) # revealed: type & ~type[OnlyClassmethodMembers] +62 | def g(arg: object, arg2: type): +63 | isinstance(arg, (HasX, RuntimeCheckableHasX)) # error: [isinstance-against-protocol] +64 | isinstance(arg, (HasX, int)) # error: [isinstance-against-protocol] +65 | +66 | # error: [isinstance-against-protocol] +67 | # error: [isinstance-against-protocol] +68 | issubclass(arg2, (HasX, RuntimeCheckableHasX)) +69 | +70 | issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol] +71 | def g2(arg: object, arg2: type): +72 | isinstance(arg, (int, (HasX, str))) # error: [isinstance-against-protocol] +73 | +74 | # error: [isinstance-against-protocol] +75 | # error: [isinstance-against-protocol] +76 | issubclass(arg2, (int, (HasX, RuntimeCheckableHasX))) +77 | classes = (HasX, int) +78 | +79 | def h(arg: object): +80 | isinstance(arg, classes) # error: [isinstance-against-protocol] ``` # Diagnostics @@ -179,3 +198,229 @@ info: `MultipleNonMethodMembers` has non-method members `a` and `b` info: rule `isinstance-against-protocol` is enabled by default ``` + +``` +error[isinstance-against-protocol]: Class `HasX` cannot be used as the second argument to `isinstance` + --> src/mdtest_snippet.py:63:5 + | +61 | reveal_type(arg1) # revealed: type & ~type[OnlyClassmethodMembers] +62 | def g(arg: object, arg2: type): +63 | isinstance(arg, (HasX, RuntimeCheckableHasX)) # error: [isinstance-against-protocol] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +64 | isinstance(arg, (HasX, int)) # error: [isinstance-against-protocol] + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `isinstance` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `isinstance-against-protocol` is enabled by default + +``` + +``` +error[isinstance-against-protocol]: Class `HasX` cannot be used as the second argument to `isinstance` + --> src/mdtest_snippet.py:64:5 + | +62 | def g(arg: object, arg2: type): +63 | isinstance(arg, (HasX, RuntimeCheckableHasX)) # error: [isinstance-against-protocol] +64 | isinstance(arg, (HasX, int)) # error: [isinstance-against-protocol] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +65 | +66 | # error: [isinstance-against-protocol] + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `isinstance` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `isinstance-against-protocol` is enabled by default + +``` + +``` +error[isinstance-against-protocol]: Class `HasX` cannot be used as the second argument to `issubclass` + --> src/mdtest_snippet.py:68:5 + | +66 | # error: [isinstance-against-protocol] +67 | # error: [isinstance-against-protocol] +68 | issubclass(arg2, (HasX, RuntimeCheckableHasX)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +69 | +70 | issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol] + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `issubclass` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `isinstance-against-protocol` is enabled by default + +``` + +``` +error[isinstance-against-protocol]: Class `RuntimeCheckableHasX` cannot be used as the second argument to `issubclass` + --> src/mdtest_snippet.py:68:5 + | +66 | # error: [isinstance-against-protocol] +67 | # error: [isinstance-against-protocol] +68 | issubclass(arg2, (HasX, RuntimeCheckableHasX)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +69 | +70 | issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol] + | +info: A protocol class cannot be used in `issubclass` checks if it has non-method members + --> src/mdtest_snippet.py:20:5 + | +18 | @runtime_checkable +19 | class RuntimeCheckableHasX(Protocol): +20 | x: int + | ^ Non-method member `x` declared here +21 | +22 | def f(arg: object): + | +info: rule `isinstance-against-protocol` is enabled by default + +``` + +``` +error[isinstance-against-protocol]: Class `HasX` cannot be used as the second argument to `issubclass` + --> src/mdtest_snippet.py:70:5 + | +68 | issubclass(arg2, (HasX, RuntimeCheckableHasX)) +69 | +70 | issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +71 | def g2(arg: object, arg2: type): +72 | isinstance(arg, (int, (HasX, str))) # error: [isinstance-against-protocol] + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `issubclass` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `isinstance-against-protocol` is enabled by default + +``` + +``` +error[isinstance-against-protocol]: Class `HasX` cannot be used as the second argument to `isinstance` + --> src/mdtest_snippet.py:72:5 + | +70 | issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol] +71 | def g2(arg: object, arg2: type): +72 | isinstance(arg, (int, (HasX, str))) # error: [isinstance-against-protocol] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +73 | +74 | # error: [isinstance-against-protocol] + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `isinstance` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `isinstance-against-protocol` is enabled by default + +``` + +``` +error[isinstance-against-protocol]: Class `HasX` cannot be used as the second argument to `issubclass` + --> src/mdtest_snippet.py:76:5 + | +74 | # error: [isinstance-against-protocol] +75 | # error: [isinstance-against-protocol] +76 | issubclass(arg2, (int, (HasX, RuntimeCheckableHasX))) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +77 | classes = (HasX, int) + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `issubclass` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `isinstance-against-protocol` is enabled by default + +``` + +``` +error[isinstance-against-protocol]: Class `RuntimeCheckableHasX` cannot be used as the second argument to `issubclass` + --> src/mdtest_snippet.py:76:5 + | +74 | # error: [isinstance-against-protocol] +75 | # error: [isinstance-against-protocol] +76 | issubclass(arg2, (int, (HasX, RuntimeCheckableHasX))) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +77 | classes = (HasX, int) + | +info: A protocol class cannot be used in `issubclass` checks if it has non-method members + --> src/mdtest_snippet.py:20:5 + | +18 | @runtime_checkable +19 | class RuntimeCheckableHasX(Protocol): +20 | x: int + | ^ Non-method member `x` declared here +21 | +22 | def f(arg: object): + | +info: rule `isinstance-against-protocol` is enabled by default + +``` + +``` +error[isinstance-against-protocol]: Class `HasX` cannot be used as the second argument to `isinstance` + --> src/mdtest_snippet.py:80:5 + | +79 | def h(arg: object): +80 | isinstance(arg, classes) # error: [isinstance-against-protocol] + | ^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `isinstance` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `isinstance-against-protocol` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index c1123dd3c024a..9fa17eddf669b 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -1917,6 +1917,20 @@ def _(obj: object, obj2: type): issubclass(obj2, Person) ``` +The same applies when a `TypedDict` class appears inside a tuple, including non-literal tuples: + +```py +def _(obj: object, obj2: type): + isinstance(obj, (int, Person)) # error: [isinstance-against-typed-dict] + issubclass(obj2, (int, Person)) # error: [isinstance-against-typed-dict] + isinstance(obj, (int, (str, Person))) # error: [isinstance-against-typed-dict] + +classes = (int, Person) + +def _(obj: object): + isinstance(obj, classes) # error: [isinstance-against-typed-dict] +``` + They also cannot be used in class patterns for `match` statements: ```py diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 9e14bf373e3c0..d6b1f4ef8b8fb 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1260,6 +1260,186 @@ impl<'db> FunctionType<'db> { } } +/// Check the second argument to `isinstance()` or `issubclass()` for types that cannot be used +/// at runtime (protocol classes, typed dicts, `typing.Any` in `isinstance`, and invalid +/// `UnionType` elements). Handles class literals, tuples (including nested tuples), and +/// recursively validates each element. +/// +/// `classinfo_expr` is the AST expression corresponding to `classinfo`, if available. It is +/// used for precise annotation spans (e.g., highlighting just the `UnionType` inside a tuple +/// rather than the whole tuple). It may be `None` when the tuple is not a literal in the AST +/// (e.g., when it's stored in a variable). +fn check_classinfo_in_isinstance<'db>( + db: &'db dyn Db, + context: &InferContext<'db, '_>, + call_expression: &ast::ExprCall, + function: KnownFunction, + classinfo: Type<'db>, + classinfo_expr: Option<&ast::Expr>, +) { + match classinfo { + Type::ClassLiteral(class) => { + if class.is_typed_dict(db) { + report_runtime_check_against_typed_dict(context, call_expression, class, function); + } else if let Some(protocol_class) = class.into_protocol_class(db) { + if !protocol_class.is_runtime_checkable(db) { + report_runtime_check_against_non_runtime_checkable_protocol( + context, + call_expression, + protocol_class, + function, + ); + } else if function == KnownFunction::IsSubclass { + let non_method_members = protocol_class.interface(db).non_method_members(db); + if !non_method_members.is_empty() { + report_issubclass_check_against_protocol_with_non_method_members( + context, + call_expression, + protocol_class, + &non_method_members, + ); + } + } + } + } + Type::SpecialForm(SpecialFormType::Any) if function == KnownFunction::IsInstance => { + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression) else { + return; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "`typing.Any` cannot be used with `isinstance()`" + )); + diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + } + Type::KnownInstance(KnownInstanceType::UnionType(_)) => { + report_invalid_union_type_elements( + db, + context, + call_expression, + function, + classinfo, + classinfo_expr, + ); + } + Type::NominalInstance(nominal) => { + if let Some(tuple_spec) = nominal.tuple_spec(db) { + let element_exprs = match classinfo_expr { + Some(ast::Expr::Tuple(tuple_expr)) => Some(&tuple_expr.elts), + _ => None, + }; + for (index, element) in tuple_spec.iter_all_elements().enumerate() { + let element_expr = element_exprs.and_then(|elts| elts.get(index)); + check_classinfo_in_isinstance( + db, + context, + call_expression, + function, + element, + element_expr, + ); + } + } + } + _ => {} + } +} + +/// Report an error if a `types.UnionType` instance passed to `isinstance()`/`issubclass()` +/// contains elements that are not class objects. +fn report_invalid_union_type_elements<'db>( + db: &'db dyn Db, + context: &InferContext<'db, '_>, + call_expression: &ast::ExprCall, + function: KnownFunction, + union_type: Type<'db>, + union_type_expr: Option<&ast::Expr>, +) { + fn find_invalid_elements<'db>( + db: &'db dyn Db, + function: KnownFunction, + ty: Type<'db>, + invalid_elements: &mut Vec>, + ) { + match ty { + Type::ClassLiteral(_) => {} + Type::NominalInstance(instance) + if instance.has_known_class(db, KnownClass::NoneType) => {} + Type::SpecialForm(special_form) if special_form.is_valid_isinstance_target() => {} + // `Any` can be used in `issubclass()` calls but not `isinstance()` calls + Type::SpecialForm(SpecialFormType::Any) if function == KnownFunction::IsSubclass => {} + Type::KnownInstance(KnownInstanceType::UnionType(instance)) => { + match instance.value_expression_types(db) { + Ok(value_expression_types) => { + for element in value_expression_types { + find_invalid_elements(db, function, element, invalid_elements); + } + } + Err(_) => { + invalid_elements.push(ty); + } + } + } + _ => invalid_elements.push(ty), + } + } + + let mut invalid_elements = vec![]; + find_invalid_elements(db, function, union_type, &mut invalid_elements); + + let Some((first_invalid_element, other_invalid_elements)) = invalid_elements.split_first() + else { + return; + }; + + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression) else { + return; + }; + + let function_name: &str = function.into(); + + let mut diagnostic = + builder.into_diagnostic(format_args!("Invalid second argument to `{function_name}`")); + diagnostic.info(format_args!( + "A `UnionType` instance can only be used as the second argument to \ + `{function_name}` if all elements are class objects" + )); + if let Some(union_type_expr) = union_type_expr { + diagnostic.annotate( + Annotation::secondary(context.span(union_type_expr)) + .message("This `UnionType` instance contains non-class elements"), + ); + } + + // When we have a secondary annotation pointing at the UnionType expression, + // "the union" is unambiguous. Otherwise, spell out the union type in the message. + let union_suffix = match (&union_type_expr, union_type) { + (None, Type::KnownInstance(KnownInstanceType::UnionType(instance))) => { + match instance.union_type(db) { + Ok(ty) => format!(" `{}`", ty.display(db)), + Err(_) => String::new(), + } + } + _ => String::new(), + }; + + match other_invalid_elements { + [] => diagnostic.info(format_args!( + "Element `{}` in the union{union_suffix} is not a class object", + first_invalid_element.display(db) + )), + [single] => diagnostic.info(format_args!( + "Elements `{}` and `{}` in the union{union_suffix} are not class objects", + first_invalid_element.display(db), + single.display(db), + )), + _ => diagnostic.info(format_args!( + "Element `{}` in the union{union_suffix}, and {} more elements, are not class objects", + first_invalid_element.display(db), + other_invalid_elements.len(), + )), + } +} + /// Evaluate an `isinstance` call. Return `Truthiness::AlwaysTrue` if we can definitely infer that /// this will return `True` at runtime, `Truthiness::AlwaysFalse` if we can definitely infer /// that this will return `False` at runtime, or `Truthiness::Ambiguous` if we should infer `bool` @@ -1980,145 +2160,21 @@ impl KnownFunction { return; }; - match second_argument { - Type::ClassLiteral(class) => { - if class.is_typed_dict(db) { - report_runtime_check_against_typed_dict( - context, - call_expression, - *class, - self, - ); - } else if let Some(protocol_class) = class.into_protocol_class(db) { - if !protocol_class.is_runtime_checkable(db) { - report_runtime_check_against_non_runtime_checkable_protocol( - context, - call_expression, - protocol_class, - self, - ); - } else if self == KnownFunction::IsSubclass { - let non_method_members = - protocol_class.interface(db).non_method_members(db); - if !non_method_members.is_empty() { - report_issubclass_check_against_protocol_with_non_method_members( - context, - call_expression, - protocol_class, - &non_method_members, - ); - } - } - } - - if self == KnownFunction::IsInstance { - overload.set_return_type( - is_instance_truthiness(db, *first_arg, *class).into_type(db), - ); - } - } - // The special-casing here is necessary because we recognise the symbol `typing.Any` as an - // instance of `type` at runtime. Even once we understand typeshed's annotation for - // `isinstance()`, we'd continue to accept calls such as `isinstance(x, typing.Any)` without - // emitting a diagnostic if we didn't have this branch. - Type::SpecialForm(SpecialFormType::Any) - if self == KnownFunction::IsInstance => - { - let Some(builder) = - context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression) - else { - return; - }; - let mut diagnostic = builder.into_diagnostic(format_args!( - "`typing.Any` cannot be used with `isinstance()`" - )); - diagnostic - .set_primary_message("This call will raise `TypeError` at runtime"); - } - - Type::KnownInstance(KnownInstanceType::UnionType(_)) => { - fn find_invalid_elements<'db>( - db: &'db dyn Db, - function: KnownFunction, - ty: Type<'db>, - invalid_elements: &mut Vec>, - ) { - match ty { - Type::ClassLiteral(_) => {} - Type::NominalInstance(instance) - if instance.has_known_class(db, KnownClass::NoneType) => {} - Type::SpecialForm(special_form) - if special_form.is_valid_isinstance_target() => {} - // `Any` can be used in `issubclass()` calls but not `isinstance()` calls - Type::SpecialForm(SpecialFormType::Any) - if function == KnownFunction::IsSubclass => {} - Type::KnownInstance(KnownInstanceType::UnionType(instance)) => { - match instance.value_expression_types(db) { - Ok(value_expression_types) => { - for element in value_expression_types { - find_invalid_elements( - db, - function, - element, - invalid_elements, - ); - } - } - Err(_) => { - invalid_elements.push(ty); - } - } - } - _ => invalid_elements.push(ty), - } - } - - let mut invalid_elements = vec![]; - find_invalid_elements(db, self, *second_argument, &mut invalid_elements); - - let Some((first_invalid_element, other_invalid_elements)) = - invalid_elements.split_first() - else { - return; - }; - - let Some(builder) = - context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression) - else { - return; - }; - - let function_name: &str = self.into(); - - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid second argument to `{function_name}`" - )); - diagnostic.info(format_args!( - "A `UnionType` instance can only be used as the second argument to \ - `{function_name}` if all elements are class objects" - )); - diagnostic.annotate( - Annotation::secondary(context.span(&call_expression.arguments.args[1])) - .message("This `UnionType` instance contains non-class elements"), - ); - match other_invalid_elements { - [] => diagnostic.info(format_args!( - "Element `{}` in the union is not a class object", - first_invalid_element.display(db) - )), - [single] => diagnostic.info(format_args!( - "Elements `{}` and `{}` in the union are not class objects", - first_invalid_element.display(db), - single.display(db), - )), - _ => diagnostic.info(format_args!( - "Element `{}` in the union, and {} more elements, are not class objects", - first_invalid_element.display(db), - other_invalid_elements.len(), - )) - } - } - _ => {} + check_classinfo_in_isinstance( + db, + context, + call_expression, + self, + *second_argument, + call_expression.arguments.args.get(1), + ); + + if let Type::ClassLiteral(class) = second_argument + && self == KnownFunction::IsInstance + { + overload.set_return_type( + is_instance_truthiness(db, *first_arg, *class).into_type(db), + ); } } From df7e8268589df98ab5f1a989d46dd3e22d9cbffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E5=81=9A=E4=BA=86=E7=9D=A1=E5=A4=A7=E8=A7=89?= <64798754+stakeswky@users.noreply.github.com> Date: Sat, 28 Feb 2026 01:19:30 +0800 Subject: [PATCH 122/261] [`fastapi`] Handle callable class dependencies with `__call__` method (`FAST003`) (#23553) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #23526 ## Problem When a class with a `__call__` method (but no `__init__`) is used as a FastAPI dependency, FAST003 emits a false positive: ```python class Query: def __call__(self, thing_id: int): pass @app.get("/things/{thing_id}") async def read_thing(query: Annotated[str, Depends(Query)]): ... # FAST003: Parameter `thing_id` appears in route path, but not in `read_thing` signature ``` ## Root Cause In `from_dependency_name`, the `ClassDefinition` branch checked for Pydantic base models and `__init__`, but returned `None` (not `Some(Self::Unknown)`) when neither was found. This caused the dependency to be silently skipped in the caller, leaving the path parameter unmatched. ## Fix Two changes: 1. **Fall back to `__call__`** when no `__init__` is found. This correctly handles the [callable instance pattern](https://fastapi.tiangolo.com/advanced/advanced-dependencies/) from FastAPI's docs, where an instance with `__call__` is passed to `Depends`. 2. **Return `Some(Self::Unknown)`** instead of `None` when neither `__init__` nor `__call__` exists, so we conservatively suppress the diagnostic rather than emitting a false positive. ## Tests Added four new test cases: - Callable class with `__call__(self, thing_id)` → no diagnostic ✓ - Class with both `__init__(self, thing_id)` and `__call__` → no diagnostic (uses `__init__`) ✓ - Callable class where path param is NOT in `__call__` → FAST003 emitted ✓ - Empty class (no `__init__`, no `__call__`) → no diagnostic (Unknown) ✓ --------- Co-authored-by: stakeswky Co-authored-by: User Co-authored-by: Brent Westbrook --- .../test/fixtures/fastapi/FAST003.py | 70 ++++++++++ .../rules/fastapi_unused_path_parameter.rs | 120 ++++++++++++------ ...-api-unused-path-parameter_FAST003.py.snap | 80 +++++++++++- ...-api-unused-path-parameter_FAST003.py.snap | 17 +++ 4 files changed, 247 insertions(+), 40 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py index f8c8e71bf09d8..dd73021c2a294 100644 --- a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py +++ b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py @@ -299,3 +299,73 @@ async def read_thing_posonly_default_trailing(query: str = "", /,): ... @app.get("/things/{thing_id}") async def read_thing_posonly_with_regular(query: str = "", /, x=None): ... + + +# https://github.com/astral-sh/ruff/issues/23526 + +# Error: `Depends(CallableQuery)` passes the class itself, so FastAPI uses +# `__init__` (which has no params here), not `__call__`. The path parameter +# `thing_id` is unused. +class CallableQuery: + def __call__(self, thing_id: int): + pass + + +@app.get("/things/{thing_id}") +async def read_thing_callable_dep(query: Annotated[str, Depends(CallableQuery)]): ... + + +# OK: `Depends(CallableQuery())` passes an instance, so FastAPI uses `__call__`, +# which declares `thing_id`. +@app.get("/things/{thing_id}") +async def read_thing_callable_dep_instance(query: Annotated[str, Depends(CallableQuery())]): ... + + +# OK: class with both __init__ and __call__, passed as class reference. +# FastAPI uses `__init__`, which declares `thing_id`. +class InitAndCallQuery: + def __init__(self, thing_id: int): + pass + + def __call__(self, other: str): + pass + + +@app.get("/things/{thing_id}") +async def read_thing_init_and_call_dep(query: Annotated[str, Depends(InitAndCallQuery)]): ... + + +# Error: `Depends(CallableQueryOther)` — class reference, uses `__init__` (no +# params). `thing_id` is unused. +class CallableQueryOther: + def __call__(self, other: str): + pass + + +@app.get("/things/{thing_id}") +async def read_thing_callable_dep_missing(query: Annotated[str, Depends(CallableQueryOther)]): ... + + +# Error: `Depends(InitAndCallQuery())` passes an instance, so FastAPI uses +# `__call__`, which has `other` — not `thing_id`. +@app.get("/things/{thing_id}") +async def read_thing_init_and_call_instance(query: Annotated[str, Depends(InitAndCallQuery())]): ... + + +# Error: class with no __init__ and no __call__; FastAPI calls __init__ which +# has no parameters, so `thing_id` is not covered by the dependency. +class EmptyClass: + pass + + +@app.get("/things/{thing_id}") +async def read_thing_empty_class_dep(query: Annotated[str, Depends(EmptyClass)]): ... + + +# Same instance patterns as default values (not Annotated). +# OK: `__call__` declares `thing_id`. +@app.get("/things/{thing_id}") +async def read_thing_callable_dep_instance_default(query: str = Depends(CallableQuery())): ... +# Error: `__call__` has `other`, not `thing_id`. +@app.get("/things/{thing_id}") +async def read_thing_init_and_call_instance_default(query: str = Depends(InitAndCallQuery())): ... diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs index 7dad14f1f8ba7..24be9c37dfc5a 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs @@ -307,13 +307,27 @@ impl<'a> Dependency<'a> { } fn from_depends_call(arguments: &'a Arguments, semantic: &SemanticModel<'a>) -> Option { - let Some(Expr::Name(name)) = arguments.find_argument_value("dependency", 0) else { - return None; - }; - - Self::from_dependency_name(name, semantic) + let dep_arg = arguments.find_argument_value("dependency", 0)?; + + match dep_arg { + // `Depends(some_callable)` — a name reference (function or class). + Expr::Name(name) => Self::from_dependency_name(name, semantic), + // `Depends(SomeClass(...))` — a call expression. If the callee is a + // class, FastAPI will invoke `__call__` on the resulting instance. + Expr::Call(call) => { + let Expr::Name(name) = call.func.as_ref() else { + return None; + }; + Self::from_dependency_instance(name, semantic) + } + _ => None, + } } + /// Resolve a dependency that is a name reference (e.g. `Depends(Query)`). + /// + /// For classes, FastAPI calls the class constructor, so the parameters come + /// from `__init__`. fn from_dependency_name(name: &'a ast::ExprName, semantic: &SemanticModel<'a>) -> Option { let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { return Some(Self::Unknown); @@ -334,46 +348,74 @@ impl<'a> Dependency<'a> { Some(Self::Function(parameter_names)) } BindingKind::ClassDefinition(scope_id) => { - let scope = &semantic.scopes[scope_id]; - - let ScopeKind::Class(class_def) = scope.kind else { - return Some(Self::Unknown); - }; + Self::class_params_from_method(semantic, scope_id, "__init__") + } + _ => Some(Self::Unknown), + } + } - let parameter_names = if class_def - .bases() - .iter() - .any(|expr| is_pydantic_base_model(expr, semantic)) - { - class_def - .body - .iter() - .filter_map(|stmt| { - stmt.as_ann_assign_stmt() - .and_then(|ann_assign| ann_assign.target.as_name_expr()) - .map(|name| name.id.as_str()) - }) - .collect() - } else if let Some(init_def) = class_def - .body - .iter() - .filter_map(|stmt| stmt.as_function_def_stmt()) - .find(|func_def| func_def.name.as_str() == "__init__") - { - // Skip `self` parameter - non_posonly_non_variadic_parameters(init_def) - .skip(1) - .map(|param| param.name().as_str()) - .collect() - } else { - return None; - }; + /// Resolve a dependency that is a class instance (e.g. `Depends(Query())`). + /// + /// FastAPI calls the instance, so the parameters come from `__call__`. + fn from_dependency_instance( + name: &'a ast::ExprName, + semantic: &SemanticModel<'a>, + ) -> Option { + let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { + return Some(Self::Unknown); + }; - Some(Self::Class(parameter_names)) + match binding.kind { + BindingKind::ClassDefinition(scope_id) => { + Self::class_params_from_method(semantic, scope_id, "__call__") } _ => Some(Self::Unknown), } } + + /// Extract parameters from a specific method (`__init__` or `__call__`) of a class. + fn class_params_from_method( + semantic: &SemanticModel<'a>, + scope_id: ruff_python_semantic::ScopeId, + method_name: &str, + ) -> Option { + let scope = &semantic.scopes[scope_id]; + + let ScopeKind::Class(class_def) = scope.kind else { + return Some(Self::Unknown); + }; + + let parameter_names = if class_def + .bases() + .iter() + .any(|expr| is_pydantic_base_model(expr, semantic)) + { + class_def + .body + .iter() + .filter_map(|stmt| { + stmt.as_ann_assign_stmt() + .and_then(|ann_assign| ann_assign.target.as_name_expr()) + .map(|name| name.id.as_str()) + }) + .collect() + } else if let Some(method_def) = class_def + .body + .iter() + .filter_map(|stmt| stmt.as_function_def_stmt()) + .find(|func_def| func_def.name.as_str() == method_name) + { + // Skip `self` parameter + non_posonly_non_variadic_parameters(method_def) + .skip(1) + .map(|param| param.name().as_str()) + .collect() + } else { + return None; + }; + + Some(Self::Class(parameter_names)) + } } fn depends_arguments<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a Arguments> { diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__deferred_annotations_diff_fast-api-unused-path-parameter_FAST003.py.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__deferred_annotations_diff_fast-api-unused-path-parameter_FAST003.py.snap index 2feae227dbade..329ca6af8244a 100644 --- a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__deferred_annotations_diff_fast-api-unused-path-parameter_FAST003.py.snap +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__deferred_annotations_diff_fast-api-unused-path-parameter_FAST003.py.snap @@ -6,7 +6,7 @@ source: crates/ruff_linter/src/rules/fastapi/mod.rs +linter.unresolved_target_version = 3.14 --- Summary --- -Removed: 3 +Removed: 7 Added: 0 --- Removed --- @@ -72,3 +72,81 @@ help: Add `id` to function signature 204 | async def get_id_init_not_annotated(params = Depends(InitParams)): ... 205 | note: This is an unsafe fix and may change runtime behavior + + +FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing_callable_dep` signature + --> FAST003.py:314:19 + | +314 | @app.get("/things/{thing_id}") + | ^^^^^^^^^^ +315 | async def read_thing_callable_dep(query: Annotated[str, Depends(CallableQuery)]): ... + | +help: Add `thing_id` to function signature +312 | +313 | +314 | @app.get("/things/{thing_id}") + - async def read_thing_callable_dep(query: Annotated[str, Depends(CallableQuery)]): ... +315 + async def read_thing_callable_dep(query: Annotated[str, Depends(CallableQuery)], thing_id): ... +316 | +317 | +318 | # OK: `Depends(CallableQuery())` passes an instance, so FastAPI uses `__call__`, +note: This is an unsafe fix and may change runtime behavior + + +FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing_callable_dep_missing` signature + --> FAST003.py:345:19 + | +345 | @app.get("/things/{thing_id}") + | ^^^^^^^^^^ +346 | async def read_thing_callable_dep_missing(query: Annotated[str, Depends(CallableQueryOther)]): ... + | +help: Add `thing_id` to function signature +343 | +344 | +345 | @app.get("/things/{thing_id}") + - async def read_thing_callable_dep_missing(query: Annotated[str, Depends(CallableQueryOther)]): ... +346 + async def read_thing_callable_dep_missing(query: Annotated[str, Depends(CallableQueryOther)], thing_id): ... +347 | +348 | +349 | # Error: `Depends(InitAndCallQuery())` passes an instance, so FastAPI uses +note: This is an unsafe fix and may change runtime behavior + + +FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing_init_and_call_instance` signature + --> FAST003.py:351:19 + | +349 | # Error: `Depends(InitAndCallQuery())` passes an instance, so FastAPI uses +350 | # `__call__`, which has `other` — not `thing_id`. +351 | @app.get("/things/{thing_id}") + | ^^^^^^^^^^ +352 | async def read_thing_init_and_call_instance(query: Annotated[str, Depends(InitAndCallQuery())]): ... + | +help: Add `thing_id` to function signature +349 | # Error: `Depends(InitAndCallQuery())` passes an instance, so FastAPI uses +350 | # `__call__`, which has `other` — not `thing_id`. +351 | @app.get("/things/{thing_id}") + - async def read_thing_init_and_call_instance(query: Annotated[str, Depends(InitAndCallQuery())]): ... +352 + async def read_thing_init_and_call_instance(query: Annotated[str, Depends(InitAndCallQuery())], thing_id): ... +353 | +354 | +355 | # Error: class with no __init__ and no __call__; FastAPI calls __init__ which +note: This is an unsafe fix and may change runtime behavior + + +FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing_empty_class_dep` signature + --> FAST003.py:361:19 + | +361 | @app.get("/things/{thing_id}") + | ^^^^^^^^^^ +362 | async def read_thing_empty_class_dep(query: Annotated[str, Depends(EmptyClass)]): ... + | +help: Add `thing_id` to function signature +359 | +360 | +361 | @app.get("/things/{thing_id}") + - async def read_thing_empty_class_dep(query: Annotated[str, Depends(EmptyClass)]): ... +362 + async def read_thing_empty_class_dep(query: Annotated[str, Depends(EmptyClass)], thing_id): ... +363 | +364 | +365 | # Same instance patterns as default values (not Annotated). +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap index e14ac7df64006..1efcffb74526e 100644 --- a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap @@ -517,3 +517,20 @@ FAST003 Parameter `thing_id` appears in route path, but not in `read_thing_poson 301 | async def read_thing_posonly_with_regular(query: str = "", /, x=None): ... | help: Add `thing_id` to function signature + +FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing_init_and_call_instance_default` signature + --> FAST003.py:370:19 + | +368 | async def read_thing_callable_dep_instance_default(query: str = Depends(CallableQuery())): ... +369 | # Error: `__call__` has `other`, not `thing_id`. +370 | @app.get("/things/{thing_id}") + | ^^^^^^^^^^ +371 | async def read_thing_init_and_call_instance_default(query: str = Depends(InitAndCallQuery())): ... + | +help: Add `thing_id` to function signature +368 | async def read_thing_callable_dep_instance_default(query: str = Depends(CallableQuery())): ... +369 | # Error: `__call__` has `other`, not `thing_id`. +370 | @app.get("/things/{thing_id}") + - async def read_thing_init_and_call_instance_default(query: str = Depends(InitAndCallQuery())): ... +371 + async def read_thing_init_and_call_instance_default(thing_id, query: str = Depends(InitAndCallQuery())): ... +note: This is an unsafe fix and may change runtime behavior From 8d956e0415ecce1fe04130a4679619e3269f8fca Mon Sep 17 00:00:00 2001 From: kar-ganap Date: Fri, 27 Feb 2026 10:08:58 -0800 Subject: [PATCH 123/261] [`pyflakes`] Fix false positive for names shadowing re-exports (`F811`) (#23356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #10874. In stub files, explicit re-exports (`from x import y as y`) at module scope were falsely flagged as redefined (F811) by class-scoped attributes with the same name. A class attribute binding in a nested scope should not invalidate a module-level re-export. Skip the F811 diagnostic when the shadowed binding is an explicit re-export (`is_explicit_export()`). ## Test plan - Reproduction case: `ruff check --select F811 stub.pyi` no longer emits false positive - `cargo test -p ruff_linter -- pyflakes` — all 462 tests pass --- .../resources/test/fixtures/pyflakes/F811_33.pyi | 7 +++++++ crates/ruff_linter/src/rules/pyflakes/mod.rs | 1 + .../src/rules/pyflakes/rules/redefined_while_unused.rs | 7 +++++++ ...f_linter__rules__pyflakes__tests__F811_F811_33.pyi.snap | 4 ++++ 4 files changed, 19 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.pyi create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_33.pyi.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.pyi b/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.pyi new file mode 100644 index 0000000000000..3454d4c1c5e2b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.pyi @@ -0,0 +1,7 @@ +# Regression test for https://github.com/astral-sh/ruff/issues/10874 +# Explicit re-exports at module scope should not be flagged as redefined +# by class-scoped attributes with the same name. +from x import y as y + +class Foo: + y = 42 # OK — class attribute, different scope from module-level re-export diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 53d8958e58fa1..fb2a051ef3842 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -132,6 +132,7 @@ mod tests { #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_30.py"))] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_31.py"))] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_32.py"))] + #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_33.pyi"))] #[test_case(Rule::UndefinedName, Path::new("F821_0.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_1.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_2.py"))] diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs b/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs index 09dd2d460e4f2..c93d0fc8c6029 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs @@ -126,6 +126,13 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope ) { continue; } + + // Don't flag explicit re-exports (e.g., `from x import y as y`). + // A binding in a nested scope (like a class attribute) doesn't + // invalidate a module-level re-export. + if shadowed.is_explicit_export() { + continue; + } } // If the bindings are in different forks, abort. diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_33.pyi.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_33.pyi.snap new file mode 100644 index 0000000000000..d0b409f39ee0b --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_33.pyi.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- + From 76906cc6388f167b937992cb007b0611c54db2ce Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Fri, 27 Feb 2026 18:46:10 +0000 Subject: [PATCH 124/261] Bump cargo dist to 0.31 (#23614) --- .github/workflows/release.yml | 2 +- dist-workspace.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 967686b77ca01..938b16210611f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,7 +68,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: diff --git a/dist-workspace.toml b/dist-workspace.toml index 8df1e925ade49..b79f6f2728f73 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -5,7 +5,7 @@ packages = ["ruff"] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.30.2" +cargo-dist-version = "0.31.0" # Whether to consider the binaries in a package for distribution (defaults true) dist = false # CI backends to support From b02cdacc1b54ac3b7649f20267b0746077e8e022 Mon Sep 17 00:00:00 2001 From: Anish Giri <161533316+anishgirianish@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:59:18 -0600 Subject: [PATCH 125/261] [`ruff`] Add fix for `none-not-at-end-of-union` (`RUF036`) (#22829) Co-authored-by: Amethyst Reese --- .../resources/test/fixtures/ruff/RUF036.py | 45 +++- .../resources/test/fixtures/ruff/RUF036.pyi | 13 + .../ruff/rules/none_not_at_end_of_union.rs | 135 +++++++++- ..._rules__ruff__tests__RUF036_RUF036.py.snap | 233 +++++++++++++++--- ...rules__ruff__tests__RUF036_RUF036.pyi.snap | 194 ++++++++++++--- 5 files changed, 543 insertions(+), 77 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py index 00aca49ab66bb..62d00b006509e 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py @@ -21,12 +21,53 @@ def func5() -> U[None, int]: ... -def func6(arg: U[None, None, int]): +def func6(arg: U[None, None, int]): + ... + + +# Comments in annotation (unsafe fix) +def func7() -> U[ + None, + # comment + int +]: + ... + + +# Nested unions - no fix should be provided +def func8(x: None | U[None, int]): + ... + + +def func9(x: int | (str | None) | list): + ... + + +def func10(x: U[int, U[None, list | set]]): + ... + + +# Multiple annotations in the same function +def func11(x: None | int) -> None | int: + ... + + +# With default argument (from poetry ecosystem check) +def func12(io: None | int = None) -> int | None: + ... + + +# 3+ member PEP 604 chains +def func13(arg: None | int | str): + ... + + +def func14(arg: None | int | str | bytes): ... # Ok -def good_func1(arg: int | None): +def good_func1(arg: int | None): ... diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.pyi b/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.pyi index f4210325bff49..3a9253994f589 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.pyi +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.pyi @@ -13,6 +13,19 @@ def func5() -> U[None, int]: ... def func6(arg: U[None, None, int]): ... +# Nested unions - no fix should be provided +def func7(x: None | U[None, int]): ... + +def func8(x: U[int, U[None, list | set]]): ... + +# Multiple annotations in the same function +def func9(x: None | int) -> None | int: ... + +# 3+ member PEP 604 chains +def func10(arg: None | int | str): ... + +def func11(arg: None | int | str | bytes): ... + # Ok def good_func1(arg: int | None): ... diff --git a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs index b45bd3877a1c3..564570897ae0c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs @@ -1,11 +1,13 @@ +use ruff_diagnostics::Applicability; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::Expr; +use ruff_python_ast::helpers::pep_604_union; +use ruff_python_ast::{Expr, ExprBinOp, Operator}; +use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::typing::traverse_union; -use ruff_text_size::Ranged; -use smallvec::SmallVec; +use ruff_text_size::{Ranged, TextRange}; -use crate::Violation; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for type annotations where `None` is not at the end of an union. @@ -34,33 +36,87 @@ use crate::checkers::ast::Checker; pub(crate) struct NoneNotAtEndOfUnion; impl Violation for NoneNotAtEndOfUnion { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { - "`None` not at the end of the type annotation.".to_string() + "`None` not at the end of the type union.".to_string() + } + + fn fix_title(&self) -> Option { + Some("Move `None` to the end of the type union".to_string()) + } +} + +/// Returns `true` if a union expression contains nested sub-unions that would +/// need to be flattened by a fix. +/// +/// For PEP 604 unions (`a | b | c`), the AST is left-recursive: `(a | b) | c`. +/// Only right-hand unions indicate actual nesting from parenthesization, e.g. +/// `a | (b | c)`. For `typing.Union`, any tuple element that is itself a union +/// is considered nested. +fn has_nested_union(semantic: &SemanticModel, expr: &Expr) -> bool { + match expr { + Expr::BinOp(ExprBinOp { + op: Operator::BitOr, + left, + right, + .. + }) => is_union_expr(semantic, right) || has_nested_union(semantic, left), + Expr::Subscript(subscript) if semantic.match_typing_expr(&subscript.value, "Union") => { + if let Expr::Tuple(tuple) = &*subscript.slice { + tuple.iter().any(|elt| is_union_expr(semantic, elt)) + } else { + false + } + } + _ => false, + } +} + +/// Returns `true` if `expr` is itself a union type (PEP 604 `|` or +/// `typing.Union[...]`). +fn is_union_expr(semantic: &SemanticModel, expr: &Expr) -> bool { + match expr { + Expr::BinOp(ExprBinOp { + op: Operator::BitOr, + .. + }) => true, + Expr::Subscript(subscript) => semantic.match_typing_expr(&subscript.value, "Union"), + _ => false, } } /// RUF036 pub(crate) fn none_not_at_end_of_union<'a>(checker: &Checker, union: &'a Expr) { let semantic = checker.semantic(); - let mut none_exprs: SmallVec<[&Expr; 1]> = SmallVec::new(); + let mut none_exprs: Vec<&Expr> = Vec::new(); + let mut other_exprs: Vec<&Expr> = Vec::new(); let mut last_expr: Option<&Expr> = None; - let mut find_none = |expr: &'a Expr, _parent: &Expr| { + let mut is_pep604 = false; + + let mut collect_members = |expr: &'a Expr, parent: &'a Expr| { + if !is_pep604 { + is_pep604 = matches!(parent, Expr::BinOp(_)); + } + if matches!(expr, Expr::NoneLiteral(_)) { none_exprs.push(expr); + } else { + other_exprs.push(expr); } last_expr = Some(expr); }; // Walk through all type expressions in the union and keep track of `None` literals. - traverse_union(&mut find_none, semantic, union); + traverse_union(&mut collect_members, semantic, union); let Some(last_expr) = last_expr else { return; }; - // The must be at least one `None` expression. + // There must be at least one `None` expression. let Some(last_none) = none_exprs.last() else { return; }; @@ -70,7 +126,64 @@ pub(crate) fn none_not_at_end_of_union<'a>(checker: &Checker, union: &'a Expr) { return; } - for none_expr in none_exprs { - checker.report_diagnostic(NoneNotAtEndOfUnion, none_expr.range()); + let mut diagnostic = checker.report_diagnostic(NoneNotAtEndOfUnion, union.range()); + + // Skip fix for nested unions to avoid flattening, and for PEP 604 unions + // with multiple `None`s to avoid generating `None | None`. + if has_nested_union(semantic, union) + || other_exprs.is_empty() + || is_pep604 && none_exprs.len() > 1 + { + return; + } + + if let Some(fix) = generate_fix(checker, &other_exprs, &none_exprs, union, is_pep604) { + diagnostic.set_fix(fix); } } + +fn generate_fix( + checker: &Checker, + other_exprs: &[&Expr], + none_exprs: &[&Expr], + annotation: &Expr, + is_pep604: bool, +) -> Option { + let applicability = if checker.comment_ranges().intersects(annotation.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + + let reordered: Vec = other_exprs + .iter() + .chain(none_exprs) + .copied() + .cloned() + .collect(); + + let new_expr = if is_pep604 { + pep_604_union(&reordered) + } else { + // Preserve the original subscript value (e.g., `Union`, `U`, `typing.Union`). + let Expr::Subscript(subscript) = annotation else { + return None; + }; + Expr::Subscript(ruff_python_ast::ExprSubscript { + value: subscript.value.clone(), + slice: Box::new(Expr::Tuple(ruff_python_ast::ExprTuple { + elts: reordered, + ctx: ruff_python_ast::ExprContext::Load, + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::NONE, + parenthesized: false, + })), + ctx: ruff_python_ast::ExprContext::Load, + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::NONE, + }) + }; + + let edit = Edit::range_replacement(checker.generator().expr(&new_expr), annotation.range()); + Some(Fix::applicable_edit(edit, applicability)) +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap index 5c5c432059c1f..fc03fc791a7eb 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap @@ -1,66 +1,241 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF036 `None` not at the end of the type annotation. +RUF036 [*] `None` not at the end of the type union. --> RUF036.py:4:16 | 4 | def func1(arg: None | int): - | ^^^^ + | ^^^^^^^^^^ 5 | ... | +help: Move `None` to the end of the type union +1 | from typing import Union as U +2 | +3 | + - def func1(arg: None | int): +4 + def func1(arg: int | None): +5 | ... +6 | +7 | -RUF036 `None` not at the end of the type annotation. +RUF036 [*] `None` not at the end of the type union. --> RUF036.py:8:16 | 8 | def func2() -> None | int: - | ^^^^ + | ^^^^^^^^^^ 9 | ... | +help: Move `None` to the end of the type union +5 | ... +6 | +7 | + - def func2() -> None | int: +8 + def func2() -> int | None: +9 | ... +10 | +11 | -RUF036 `None` not at the end of the type annotation. +RUF036 `None` not at the end of the type union. --> RUF036.py:12:16 | 12 | def func3(arg: None | None | int): - | ^^^^ -13 | ... - | - -RUF036 `None` not at the end of the type annotation. - --> RUF036.py:12:23 - | -12 | def func3(arg: None | None | int): - | ^^^^ + | ^^^^^^^^^^^^^^^^^ 13 | ... | +help: Move `None` to the end of the type union -RUF036 `None` not at the end of the type annotation. - --> RUF036.py:16:18 +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:16:16 | 16 | def func4(arg: U[None, int]): - | ^^^^ + | ^^^^^^^^^^^^ 17 | ... | +help: Move `None` to the end of the type union +13 | ... +14 | +15 | + - def func4(arg: U[None, int]): +16 + def func4(arg: U[int, None]): +17 | ... +18 | +19 | -RUF036 `None` not at the end of the type annotation. - --> RUF036.py:20:18 +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:20:16 | 20 | def func5() -> U[None, int]: - | ^^^^ + | ^^^^^^^^^^^^ 21 | ... | +help: Move `None` to the end of the type union +17 | ... +18 | +19 | + - def func5() -> U[None, int]: +20 + def func5() -> U[int, None]: +21 | ... +22 | +23 | -RUF036 `None` not at the end of the type annotation. - --> RUF036.py:24:18 +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:24:16 | -24 | def func6(arg: U[None, None, int]): - | ^^^^ +24 | def func6(arg: U[None, None, int]): + | ^^^^^^^^^^^^^^^^^^ 25 | ... | +help: Move `None` to the end of the type union +21 | ... +22 | +23 | + - def func6(arg: U[None, None, int]): +24 + def func6(arg: U[int, None, None]): +25 | ... +26 | +27 | -RUF036 `None` not at the end of the type annotation. - --> RUF036.py:24:24 +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:29:16 | -24 | def func6(arg: U[None, None, int]): - | ^^^^ -25 | ... +28 | # Comments in annotation (unsafe fix) +29 | def func7() -> U[ + | ________________^ +30 | | None, +31 | | # comment +32 | | int +33 | | ]: + | |_^ +34 | ... + | +help: Move `None` to the end of the type union +26 | +27 | +28 | # Comments in annotation (unsafe fix) + - def func7() -> U[ + - None, + - # comment + - int + - ]: +29 + def func7() -> U[int, None]: +30 | ... +31 | +32 | +note: This is an unsafe fix and may change runtime behavior + +RUF036 `None` not at the end of the type union. + --> RUF036.py:38:14 + | +37 | # Nested unions - no fix should be provided +38 | def func8(x: None | U[None, int]): + | ^^^^^^^^^^^^^^^^^^^ +39 | ... + | +help: Move `None` to the end of the type union + +RUF036 `None` not at the end of the type union. + --> RUF036.py:42:14 + | +42 | def func9(x: int | (str | None) | list): + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +43 | ... + | +help: Move `None` to the end of the type union + +RUF036 `None` not at the end of the type union. + --> RUF036.py:46:15 + | +46 | def func10(x: U[int, U[None, list | set]]): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +47 | ... + | +help: Move `None` to the end of the type union + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:51:15 + | +50 | # Multiple annotations in the same function +51 | def func11(x: None | int) -> None | int: + | ^^^^^^^^^^ +52 | ... + | +help: Move `None` to the end of the type union +48 | +49 | +50 | # Multiple annotations in the same function + - def func11(x: None | int) -> None | int: +51 + def func11(x: int | None) -> None | int: +52 | ... +53 | +54 | + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:51:30 + | +50 | # Multiple annotations in the same function +51 | def func11(x: None | int) -> None | int: + | ^^^^^^^^^^ +52 | ... + | +help: Move `None` to the end of the type union +48 | +49 | +50 | # Multiple annotations in the same function + - def func11(x: None | int) -> None | int: +51 + def func11(x: None | int) -> int | None: +52 | ... +53 | +54 | + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:56:16 + | +55 | # With default argument (from poetry ecosystem check) +56 | def func12(io: None | int = None) -> int | None: + | ^^^^^^^^^^ +57 | ... + | +help: Move `None` to the end of the type union +53 | +54 | +55 | # With default argument (from poetry ecosystem check) + - def func12(io: None | int = None) -> int | None: +56 + def func12(io: int | None = None) -> int | None: +57 | ... +58 | +59 | + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:61:17 + | +60 | # 3+ member PEP 604 chains +61 | def func13(arg: None | int | str): + | ^^^^^^^^^^^^^^^^ +62 | ... + | +help: Move `None` to the end of the type union +58 | +59 | +60 | # 3+ member PEP 604 chains + - def func13(arg: None | int | str): +61 + def func13(arg: int | str | None): +62 | ... +63 | +64 | + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:65:17 + | +65 | def func14(arg: None | int | str | bytes): + | ^^^^^^^^^^^^^^^^^^^^^^^^ +66 | ... | +help: Move `None` to the end of the type union +62 | ... +63 | +64 | + - def func14(arg: None | int | str | bytes): +65 + def func14(arg: int | str | bytes | None): +66 | ... +67 | +68 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.pyi.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.pyi.snap index 649e716628f01..ee19da2d0b256 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.pyi.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.pyi.snap @@ -1,88 +1,212 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF036 `None` not at the end of the type annotation. +RUF036 [*] `None` not at the end of the type union. --> RUF036.pyi:4:16 | 4 | def func1(arg: None | int): ... - | ^^^^ + | ^^^^^^^^^^ 5 | 6 | def func2() -> None | int: ... | +help: Move `None` to the end of the type union +1 | from typing import Union as U +2 | +3 | + - def func1(arg: None | int): ... +4 + def func1(arg: int | None): ... +5 | +6 | def func2() -> None | int: ... +7 | -RUF036 `None` not at the end of the type annotation. +RUF036 [*] `None` not at the end of the type union. --> RUF036.pyi:6:16 | 4 | def func1(arg: None | int): ... 5 | 6 | def func2() -> None | int: ... - | ^^^^ + | ^^^^^^^^^^ 7 | 8 | def func3(arg: None | None | int): ... | +help: Move `None` to the end of the type union +3 | +4 | def func1(arg: None | int): ... +5 | + - def func2() -> None | int: ... +6 + def func2() -> int | None: ... +7 | +8 | def func3(arg: None | None | int): ... +9 | -RUF036 `None` not at the end of the type annotation. +RUF036 `None` not at the end of the type union. --> RUF036.pyi:8:16 | 6 | def func2() -> None | int: ... 7 | 8 | def func3(arg: None | None | int): ... - | ^^^^ + | ^^^^^^^^^^^^^^^^^ 9 | 10 | def func4(arg: U[None, int]): ... | +help: Move `None` to the end of the type union -RUF036 `None` not at the end of the type annotation. - --> RUF036.pyi:8:23 +RUF036 [*] `None` not at the end of the type union. + --> RUF036.pyi:10:16 | - 6 | def func2() -> None | int: ... - 7 | 8 | def func3(arg: None | None | int): ... - | ^^^^ 9 | 10 | def func4(arg: U[None, int]): ... - | - -RUF036 `None` not at the end of the type annotation. - --> RUF036.pyi:10:18 - | - 8 | def func3(arg: None | None | int): ... - 9 | -10 | def func4(arg: U[None, int]): ... - | ^^^^ + | ^^^^^^^^^^^^ 11 | 12 | def func5() -> U[None, int]: ... | +help: Move `None` to the end of the type union +7 | +8 | def func3(arg: None | None | int): ... +9 | + - def func4(arg: U[None, int]): ... +10 + def func4(arg: U[int, None]): ... +11 | +12 | def func5() -> U[None, int]: ... +13 | -RUF036 `None` not at the end of the type annotation. - --> RUF036.pyi:12:18 +RUF036 [*] `None` not at the end of the type union. + --> RUF036.pyi:12:16 | 10 | def func4(arg: U[None, int]): ... 11 | 12 | def func5() -> U[None, int]: ... - | ^^^^ + | ^^^^^^^^^^^^ 13 | 14 | def func6(arg: U[None, None, int]): ... | +help: Move `None` to the end of the type union +9 | +10 | def func4(arg: U[None, int]): ... +11 | + - def func5() -> U[None, int]: ... +12 + def func5() -> U[int, None]: ... +13 | +14 | def func6(arg: U[None, None, int]): ... +15 | -RUF036 `None` not at the end of the type annotation. - --> RUF036.pyi:14:18 +RUF036 [*] `None` not at the end of the type union. + --> RUF036.pyi:14:16 | 12 | def func5() -> U[None, int]: ... 13 | 14 | def func6(arg: U[None, None, int]): ... - | ^^^^ + | ^^^^^^^^^^^^^^^^^^ 15 | -16 | # Ok +16 | # Nested unions - no fix should be provided | +help: Move `None` to the end of the type union +11 | +12 | def func5() -> U[None, int]: ... +13 | + - def func6(arg: U[None, None, int]): ... +14 + def func6(arg: U[int, None, None]): ... +15 | +16 | # Nested unions - no fix should be provided +17 | def func7(x: None | U[None, int]): ... -RUF036 `None` not at the end of the type annotation. - --> RUF036.pyi:14:24 +RUF036 `None` not at the end of the type union. + --> RUF036.pyi:17:14 | -12 | def func5() -> U[None, int]: ... -13 | -14 | def func6(arg: U[None, None, int]): ... - | ^^^^ -15 | -16 | # Ok +16 | # Nested unions - no fix should be provided +17 | def func7(x: None | U[None, int]): ... + | ^^^^^^^^^^^^^^^^^^^ +18 | +19 | def func8(x: U[int, U[None, list | set]]): ... + | +help: Move `None` to the end of the type union + +RUF036 `None` not at the end of the type union. + --> RUF036.pyi:19:14 + | +17 | def func7(x: None | U[None, int]): ... +18 | +19 | def func8(x: U[int, U[None, list | set]]): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +20 | +21 | # Multiple annotations in the same function + | +help: Move `None` to the end of the type union + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.pyi:22:14 + | +21 | # Multiple annotations in the same function +22 | def func9(x: None | int) -> None | int: ... + | ^^^^^^^^^^ +23 | +24 | # 3+ member PEP 604 chains + | +help: Move `None` to the end of the type union +19 | def func8(x: U[int, U[None, list | set]]): ... +20 | +21 | # Multiple annotations in the same function + - def func9(x: None | int) -> None | int: ... +22 + def func9(x: int | None) -> None | int: ... +23 | +24 | # 3+ member PEP 604 chains +25 | def func10(arg: None | int | str): ... + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.pyi:22:29 + | +21 | # Multiple annotations in the same function +22 | def func9(x: None | int) -> None | int: ... + | ^^^^^^^^^^ +23 | +24 | # 3+ member PEP 604 chains + | +help: Move `None` to the end of the type union +19 | def func8(x: U[int, U[None, list | set]]): ... +20 | +21 | # Multiple annotations in the same function + - def func9(x: None | int) -> None | int: ... +22 + def func9(x: None | int) -> int | None: ... +23 | +24 | # 3+ member PEP 604 chains +25 | def func10(arg: None | int | str): ... + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.pyi:25:17 + | +24 | # 3+ member PEP 604 chains +25 | def func10(arg: None | int | str): ... + | ^^^^^^^^^^^^^^^^ +26 | +27 | def func11(arg: None | int | str | bytes): ... + | +help: Move `None` to the end of the type union +22 | def func9(x: None | int) -> None | int: ... +23 | +24 | # 3+ member PEP 604 chains + - def func10(arg: None | int | str): ... +25 + def func10(arg: int | str | None): ... +26 | +27 | def func11(arg: None | int | str | bytes): ... +28 | + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.pyi:27:17 + | +25 | def func10(arg: None | int | str): ... +26 | +27 | def func11(arg: None | int | str | bytes): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^ +28 | +29 | # Ok | +help: Move `None` to the end of the type union +24 | # 3+ member PEP 604 chains +25 | def func10(arg: None | int | str): ... +26 | + - def func11(arg: None | int | str | bytes): ... +27 + def func11(arg: int | str | bytes | None): ... +28 | +29 | # Ok +30 | def good_func1(arg: int | None): ... From 9afd1ebda9f89d60073b6f1d72f0b137c6daade2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 27 Feb 2026 20:00:29 +0000 Subject: [PATCH 126/261] [ty] Fix bug where ty would think that a `Callable` with a variadic positional parameter could be a subtype of a `Callable` with a positional-or-keyword parameter (#23610) ## Summary On `main`, this assertion fails, but it should pass: ```py from ty_extensions import static_assert, is_subtype_of, is_assignable_to from typing import Protocol class A(Protocol): def __call__(self, *args: int | str): ... class B(Protocol): def __call__(self, a: int): ... static_assert(not is_assignable_to(A, B)) ``` A variadic positional parameter can never satisfy a positional-or-keyword parameter, because the former can never be passed a keyword argument, whereas the latter can. This fixes the only remaining conformance-suite failure on `callables_subtyping.py`. ## Test Plan mdtests --------- Co-authored-by: Claude --- .../mdtest/type_properties/is_subtype_of.md | 12 +++++++++++- crates/ty_python_semantic/src/types/signatures.rs | 12 +++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index f7cd870439132..70e4d2e57a105 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1313,7 +1313,7 @@ Variadic parameter in a subtype can only be used to match against an unmatched p parameters from the supertype, not any other parameter kind. ```py -from ty_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, is_assignable_to, static_assert def variadic(*args: int) -> None: ... @@ -1376,6 +1376,16 @@ static_assert(is_subtype_of(CallableTypeOf[variadic_a], CallableTypeOf[standard_ static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[standard_int])) ``` +A variadic positional parameter alone cannot match a positional-or-keyword parameter because +variadic positional parameters can only be called positionally. + +```py +def only_variadic(*args: int) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[only_variadic], CallableTypeOf[standard_int])) +static_assert(not is_assignable_to(CallableTypeOf[only_variadic], CallableTypeOf[standard_int])) +``` + #### Keyword-only For keyword-only parameters, the name should be the same: diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index cac7f1caf9242..4ba5644671e26 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1404,9 +1404,15 @@ impl<'db> Signature<'db> { loop { let Some(next_parameter) = parameters.next() else { - // All parameters have been checked or both the parameter lists were empty. In - // either case, `self` is a subtype of `other`. - return result; + if other_keywords.is_empty() { + // All parameters have been checked or both the parameter lists were empty. + // In either case, `self` is a subtype of `other`. + return result; + } + // There are keyword parameters in `other` that were only matched positionally + // against a variadic parameter in `self`. We need to verify that they can also + // be matched as keyword arguments, which is done after this loop. + break; }; match next_parameter { From dcab6f2dcaf0821e106db6fbdc2d0f38be6fe878 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 27 Feb 2026 21:15:17 +0100 Subject: [PATCH 127/261] [ty] Take myself out of the reviewer pool for the next few days (#23618) --- .github/pr-assignee-pools.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pr-assignee-pools.toml b/.github/pr-assignee-pools.toml index 14a9e91e318b8..742547aa17d9a 100644 --- a/.github/pr-assignee-pools.toml +++ b/.github/pr-assignee-pools.toml @@ -9,7 +9,7 @@ reviewers = ["amyreese", "ntBre"] [[pools]] name = "ty-semantic" paths = ["/crates/ty_python_semantic/**"] -reviewers = ["carljm", "sharkdp", "dcreager", "ibraheemdev", "oconnor663"] +reviewers = ["carljm", "dcreager", "ibraheemdev", "oconnor663"] [[pools]] name = "ty-module-resolver" From 2104d0a13ddb656b66e119e64bc391a517c87b49 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Sat, 28 Feb 2026 06:33:27 +0900 Subject: [PATCH 128/261] [ty] hash-cons `UseDefMap` fields (#23283) ## Summary Implement hash-consing for several fields in `UseDefMap` to reduce memory usage. The rationale for this optimization is the size and high duplication rate of the structs used in `UseDefMap`. The size of each struct is: * Bindings: 40 * PlaceState: 64 * ReachableDefinitions: 64 * EnclosingSnapshot: 40 These elements are stored within `IndexVec` where duplication of elements can occur. For example, taking these statistics (duplication rate) with hydra-zen yields the following: - bindings_by_use: 65.13% - end_of_scope_members: 57.37% - enclosing_snapshots: 57.18% - reachable_definitions_by_member: 34.58% - declarations_by_binding: 32.70% - bindings_by_definition: 27.84% - end_of_scope_symbols: 21.25% - reachable_definitions_by_symbol: 15.70% The disparity in duplication rates between symbols and members was a bit surprising, but can be explained as follows: `obj.attr` and `obj["k"]` might be tracked, but often end up in a similar undefined/unbound state, leading to a large number of identical `PlaceState` instances. This PR implements hash-consing for these fields. Specifically, each `IndexVec` for these fields will only store IDs for each struct, and the actual struct instances will be stored in another `IndexVec` without duplication. According to the memory usage report, this change reduced the size of `SemanticIndex` by about 10%. ## Performance analysis What this PR is trying to do involves a trade-off between time complexity and space complexity. In #23201, I aimed to achieve a more significant reduction in memory consumption by interning `Bindings`, but this resulted in a major regression in time performance, so I decided to manually intern only items that seemed to have a high effect. This PR change is not very effective for microbenchmarks, and rather adds a time cost (due to hash calculations). However, looking at the trends in the memory report, it seems that for large codebases, it can achieve significant memory savings with relatively little overhead. The larger the `UseDefMap`, the greater the reduction, so I think it's worth to do this. The commit that achieved the greatest memory consumption reduction in this PR was b9d067abcc4da4ce94bae5843b14416c2ed282b6, but it was reverted because it exceeded the threshold for codspeed microbenchmarks. There was almost no impact on walltime benchmarks (in fact, some even showed slight improvements). ## Test Plan N/A --- .../src/semantic_index/use_def.rs | 340 +++++++++++++++--- .../src/semantic_index/use_def/place_state.rs | 25 +- 2 files changed, 292 insertions(+), 73 deletions(-) diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index 3e8e6fed7ceab..7ecea1ada2b3e 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -161,10 +161,11 @@ //! this in the future for some closures, but for now this is where we start.) //! //! The data structure we build to answer these questions is the `UseDefMap`. It has a -//! `bindings_by_use` vector of [`Bindings`] indexed by [`ScopedUseId`], a -//! `declarations_by_binding` vector of [`Declarations`] indexed by [`ScopedDefinitionId`], a +//! `bindings_by_use` vector of [`InternedBindingsId`] indexed by [`ScopedUseId`] +//! (plus an interned bindings table), a +//! `declarations_by_binding` vector of [`InternedDeclarationsId`] indexed by [`ScopedDefinitionId`], a //! `bindings_by_declaration` vector of [`Bindings`] indexed by [`ScopedDefinitionId`], and -//! `public_bindings` and `public_definitions` vectors indexed by [`ScopedPlaceId`]. The values in +//! `end_of_scope_symbols` and `end_of_scope_members` vectors indexed by [`ScopedSymbolId`]/[`ScopedMemberId`]. The values in //! each of these vectors are (in principle) a list of live bindings at that use/definition, or at //! the end of the scope for that place, with a list of the dominating constraints for each //! binding. @@ -241,7 +242,7 @@ //! visits a `StmtIf` node. use ruff_index::{IndexVec, newtype_index}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxBuildHasher, FxHashMap}; use crate::node_key::NodeKey; use crate::place::BoundnessAnalysis; @@ -270,6 +271,35 @@ mod place_state; pub(super) use place_state::PreviousDefinitions; pub(crate) use place_state::{LiveBinding, ScopedDefinitionId}; +/// Uniquely identifies an interned [`Bindings`] entry in [`UseDefMap::interned_bindings`]. +#[newtype_index] +#[derive(salsa::Update, get_size2::GetSize)] +struct InternedBindingsId; + +/// Uniquely identifies an interned [`Declarations`] entry in [`UseDefMap::interned_declarations`]. +#[newtype_index] +#[derive(salsa::Update, get_size2::GetSize)] +struct InternedDeclarationsId; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] +struct InternedPlaceStateId(InternedBindingsId, InternedDeclarationsId); + +impl InternedPlaceStateId { + fn bindings_id(self) -> InternedBindingsId { + self.0 + } + + fn declarations_id(self) -> InternedDeclarationsId { + self.1 + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +enum InternedEnclosingSnapshotId { + Constraint(ScopedNarrowingConstraint), + Bindings(InternedBindingsId), +} + /// Applicable definitions and constraints for every use of a name. #[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(crate) struct UseDefMap<'db> { @@ -283,8 +313,13 @@ pub(crate) struct UseDefMap<'db> { /// Array of reachability constraints in this scope. reachability_constraints: ReachabilityConstraints, + /// Interned [`Bindings`] values. + interned_bindings: IndexVec, + /// Interned [`Declarations`] values. + interned_declarations: IndexVec, + /// [`Bindings`] reaching a [`ScopedUseId`]. - bindings_by_use: IndexVec, + bindings_by_use: IndexVec, /// Tracks whether or not a given AST node is reachable from the start of the scope. node_reachability: FxHashMap, @@ -295,7 +330,7 @@ pub(crate) struct UseDefMap<'db> { /// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then /// we don't actually need anything here, all we'll need to validate is that our own RHS is a /// valid assignment to our own annotation. - declarations_by_binding: FxHashMap, Declarations>, + declarations_by_binding: FxHashMap, InternedDeclarationsId>, /// If the definition is a declaration (only) -- `x: int` for example -- then we need /// [`Bindings`] to know whether this declaration is consistent with the previously @@ -307,13 +342,13 @@ pub(crate) struct UseDefMap<'db> { /// /// If we see a binding to a `Final`-qualified symbol, we also need this map to find previous /// bindings to that symbol. If there are any, the assignment is invalid. - bindings_by_definition: FxHashMap, Bindings>, + bindings_by_definition: FxHashMap, InternedBindingsId>, /// [`PlaceState`] visible at end of scope for each symbol. end_of_scope_symbols: IndexVec, /// [`PlaceState`] visible at end of scope for each member. - end_of_scope_members: IndexVec, + end_of_scope_members: IndexVec, /// All potentially reachable bindings and declarations, for each symbol. reachable_definitions_by_symbol: IndexVec, @@ -323,7 +358,7 @@ pub(crate) struct UseDefMap<'db> { /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested /// scope. - enclosing_snapshots: EnclosingSnapshots, + enclosing_snapshots: IndexVec, /// Whether or not the end of the scope is reachable. /// @@ -355,8 +390,9 @@ impl<'db> UseDefMap<'db> { &self, use_id: ScopedUseId, ) -> BindingWithConstraintsIterator<'_, 'db> { + let bindings_id = self.bindings_by_use[use_id]; self.bindings_iterator( - &self.bindings_by_use[use_id], + &self.interned_bindings[bindings_id], BoundnessAnalysis::BasedOnUnboundVisibility, ) } @@ -467,8 +503,9 @@ impl<'db> UseDefMap<'db> { &self, member: ScopedMemberId, ) -> BindingWithConstraintsIterator<'_, 'db> { + let place_state_id = self.end_of_scope_members[member]; self.bindings_iterator( - self.end_of_scope_members[member].bindings(), + &self.interned_bindings[place_state_id.bindings_id()], BoundnessAnalysis::BasedOnUnboundVisibility, ) } @@ -493,9 +530,9 @@ impl<'db> UseDefMap<'db> { pub(crate) fn reachable_member_bindings( &self, - symbol: ScopedMemberId, + member: ScopedMemberId, ) -> BindingWithConstraintsIterator<'_, 'db> { - let bindings = &self.reachable_definitions_by_member[symbol].bindings; + let bindings = &self.reachable_definitions_by_member[member].bindings; self.bindings_iterator(bindings, BoundnessAnalysis::AssumeBound) } @@ -510,13 +547,19 @@ impl<'db> UseDefMap<'db> { // TODO: We haven't implemented proper boundness analysis for nonlocal symbols, so we assume the boundness is bound for now. BoundnessAnalysis::AssumeBound }; + match self.enclosing_snapshots.get(snapshot_id) { - Some(EnclosingSnapshot::Constraint(constraint)) => { + Some(InternedEnclosingSnapshotId::Constraint(constraint)) => { EnclosingSnapshotResult::FoundConstraint(*constraint) } - Some(EnclosingSnapshot::Bindings(bindings)) => EnclosingSnapshotResult::FoundBindings( - self.bindings_iterator(bindings, boundness_analysis), - ), + Some(InternedEnclosingSnapshotId::Bindings(bindings_id)) => { + EnclosingSnapshotResult::FoundBindings( + self.bindings_iterator( + &self.interned_bindings[*bindings_id], + boundness_analysis, + ), + ) + } None => EnclosingSnapshotResult::NotFound, } } @@ -525,8 +568,9 @@ impl<'db> UseDefMap<'db> { &self, definition: Definition<'db>, ) -> BindingWithConstraintsIterator<'_, 'db> { + let bindings_id = self.bindings_by_definition[&definition]; self.bindings_iterator( - &self.bindings_by_definition[&definition], + &self.interned_bindings[bindings_id], BoundnessAnalysis::BasedOnUnboundVisibility, ) } @@ -535,8 +579,9 @@ impl<'db> UseDefMap<'db> { &self, binding: Definition<'db>, ) -> DeclarationsIterator<'_, 'db> { + let declarations_id = self.declarations_by_binding[&binding]; self.declarations_iterator( - &self.declarations_by_binding[&binding], + &self.interned_declarations[declarations_id], BoundnessAnalysis::BasedOnUnboundVisibility, ) } @@ -563,7 +608,8 @@ impl<'db> UseDefMap<'db> { &'map self, member: ScopedMemberId, ) -> DeclarationsIterator<'map, 'db> { - let declarations = self.end_of_scope_members[member].declarations(); + let place_state_id = self.end_of_scope_members[member]; + let declarations = &self.interned_declarations[place_state_id.declarations_id()]; self.declarations_iterator(declarations, BoundnessAnalysis::BasedOnUnboundVisibility) } @@ -809,7 +855,7 @@ impl<'db> Iterator for DeclarationsIterator<'_, 'db> { impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {} -#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] struct ReachableDefinitions { bindings: Bindings, declarations: Declarations, @@ -1464,21 +1510,70 @@ impl<'db> UseDefMapBuilder<'db> { .add_or_constraint(self.reachability, snapshot.reachability); } - fn mark_reachability_constraints(&mut self) { + pub(super) fn finish(mut self) -> UseDefMap<'db> { + self.all_definitions.shrink_to_fit(); + self.symbol_states.shrink_to_fit(); + self.member_states.shrink_to_fit(); + self.reachable_symbol_definitions.shrink_to_fit(); + self.reachable_member_definitions.shrink_to_fit(); + self.bindings_by_use.shrink_to_fit(); + self.node_reachability.shrink_to_fit(); + self.declarations_by_binding.shrink_to_fit(); + self.bindings_by_definition.shrink_to_fit(); + self.enclosing_snapshots.shrink_to_fit(); + + let mut interned_bindings = IndexVec::with_capacity(self.bindings_by_definition.len()); + let mut interned_ids_by_bindings = + FxHashMap::with_capacity_and_hasher(self.bindings_by_definition.len(), FxBuildHasher); + let mut interned_declarations = IndexVec::with_capacity(self.declarations_by_binding.len()); + let mut interned_ids_by_declarations = + FxHashMap::with_capacity_and_hasher(self.declarations_by_binding.len(), FxBuildHasher); + // These fields are manually interned because they have a statistically high duplication rate (>50%). + let bindings_by_definition = Self::intern_bindings_by_definition( + self.bindings_by_definition, + &mut interned_bindings, + &mut interned_ids_by_bindings, + ); + let declarations_by_binding = Self::intern_declarations_by_binding( + self.declarations_by_binding, + &mut interned_declarations, + &mut interned_ids_by_declarations, + ); + let bindings_by_use = Self::intern_bindings_by_use( + self.bindings_by_use, + &mut interned_bindings, + &mut interned_ids_by_bindings, + ); + let end_of_scope_members = Self::intern_end_of_scope_members( + self.member_states, + &mut interned_bindings, + &mut interned_ids_by_bindings, + &mut interned_declarations, + &mut interned_ids_by_declarations, + ); + let enclosing_snapshots = Self::intern_enclosing_snapshots( + self.enclosing_snapshots, + &mut interned_bindings, + &mut interned_ids_by_bindings, + ); + + interned_bindings.shrink_to_fit(); + interned_declarations.shrink_to_fit(); + // We only walk the fields that are copied through to the UseDefMap when we finish building // it. - for bindings in &mut self.bindings_by_use { + for bindings in &mut interned_bindings { bindings.finish(&mut self.reachability_constraints); } + for declarations in &mut interned_declarations { + declarations.finish(&mut self.reachability_constraints); + } for constraint in self.node_reachability.values() { self.reachability_constraints.mark_used(*constraint); } for symbol_state in &mut self.symbol_states { symbol_state.finish(&mut self.reachability_constraints); } - for member_state in &mut self.member_states { - member_state.finish(&mut self.reachability_constraints); - } for reachable_definition in &mut self.reachable_symbol_definitions { reachable_definition .bindings @@ -1495,46 +1590,183 @@ impl<'db> UseDefMapBuilder<'db> { .declarations .finish(&mut self.reachability_constraints); } - for declarations in self.declarations_by_binding.values_mut() { - declarations.finish(&mut self.reachability_constraints); - } - for bindings in self.bindings_by_definition.values_mut() { - bindings.finish(&mut self.reachability_constraints); - } - for eager_snapshot in &mut self.enclosing_snapshots { - eager_snapshot.finish(&mut self.reachability_constraints); + for enclosing_snapshot in &enclosing_snapshots { + // Bindings are already marked above. + if let InternedEnclosingSnapshotId::Constraint(constraint) = enclosing_snapshot { + self.reachability_constraints.mark_used(*constraint); + } } self.reachability_constraints.mark_used(self.reachability); - } - - pub(super) fn finish(mut self) -> UseDefMap<'db> { - self.mark_reachability_constraints(); - - self.all_definitions.shrink_to_fit(); - self.symbol_states.shrink_to_fit(); - self.member_states.shrink_to_fit(); - self.reachable_symbol_definitions.shrink_to_fit(); - self.reachable_member_definitions.shrink_to_fit(); - self.bindings_by_use.shrink_to_fit(); - self.node_reachability.shrink_to_fit(); - self.declarations_by_binding.shrink_to_fit(); - self.bindings_by_definition.shrink_to_fit(); - self.enclosing_snapshots.shrink_to_fit(); UseDefMap { all_definitions: self.all_definitions, predicates: self.predicates.build(), reachability_constraints: self.reachability_constraints.build(), - bindings_by_use: self.bindings_by_use, + interned_bindings, + interned_declarations, + bindings_by_use, node_reachability: self.node_reachability, end_of_scope_symbols: self.symbol_states, - end_of_scope_members: self.member_states, + end_of_scope_members, reachable_definitions_by_symbol: self.reachable_symbol_definitions, reachable_definitions_by_member: self.reachable_member_definitions, - declarations_by_binding: self.declarations_by_binding, - bindings_by_definition: self.bindings_by_definition, - enclosing_snapshots: self.enclosing_snapshots, + declarations_by_binding, + bindings_by_definition, + enclosing_snapshots, end_of_scope_reachability: self.reachability, } } + + fn intern_bindings_by_definition( + bindings_by_definition: FxHashMap, Bindings>, + interned_bindings: &mut IndexVec, + interned_ids_by_bindings: &mut FxHashMap, + ) -> FxHashMap, InternedBindingsId> { + let mut interned_ids_by_definition: FxHashMap, InternedBindingsId> = + FxHashMap::with_capacity_and_hasher(bindings_by_definition.len(), FxBuildHasher); + + for (definition, bindings) in bindings_by_definition { + let interned_id = if let Some(interned_id) = interned_ids_by_bindings.get(&bindings) { + *interned_id + } else { + let interned_id = interned_bindings.push(bindings.clone()); + interned_ids_by_bindings.insert(bindings, interned_id); + interned_id + }; + interned_ids_by_definition.insert(definition, interned_id); + } + + interned_ids_by_definition.shrink_to_fit(); + interned_ids_by_definition + } + + fn intern_declarations_by_binding( + declarations_by_binding: FxHashMap, Declarations>, + interned_declarations: &mut IndexVec, + interned_ids_by_declarations: &mut FxHashMap, + ) -> FxHashMap, InternedDeclarationsId> { + let mut interned_ids_by_binding: FxHashMap, InternedDeclarationsId> = + FxHashMap::with_capacity_and_hasher(declarations_by_binding.len(), FxBuildHasher); + + for (binding, declarations) in declarations_by_binding { + let interned_id = + if let Some(interned_id) = interned_ids_by_declarations.get(&declarations) { + *interned_id + } else { + let interned_id = interned_declarations.push(declarations.clone()); + interned_ids_by_declarations.insert(declarations, interned_id); + interned_id + }; + interned_ids_by_binding.insert(binding, interned_id); + } + + interned_ids_by_binding.shrink_to_fit(); + interned_ids_by_binding + } + + fn intern_bindings_by_use( + bindings_by_use: IndexVec, + interned_bindings: &mut IndexVec, + interned_ids_by_bindings: &mut FxHashMap, + ) -> IndexVec { + let mut interned_ids_by_use: IndexVec = + IndexVec::with_capacity(bindings_by_use.len()); + + for bindings in bindings_by_use { + let interned_id = if let Some(interned_id) = interned_ids_by_bindings.get(&bindings) { + *interned_id + } else { + let interned_id = interned_bindings.push(bindings.clone()); + interned_ids_by_bindings.insert(bindings, interned_id); + interned_id + }; + interned_ids_by_use.push(interned_id); + } + + interned_ids_by_use.shrink_to_fit(); + interned_ids_by_use + } + + fn intern_end_of_scope_members( + end_of_scope_members: IndexVec, + interned_bindings: &mut IndexVec, + interned_ids_by_bindings: &mut FxHashMap, + interned_declarations: &mut IndexVec, + interned_ids_by_declarations: &mut FxHashMap, + ) -> IndexVec { + let mut interned_ids_by_member: IndexVec = + IndexVec::with_capacity(end_of_scope_members.len()); + let mut interned_ids_by_place_state: FxHashMap = + FxHashMap::with_capacity_and_hasher(end_of_scope_members.len(), FxBuildHasher); + + for place_state in end_of_scope_members { + let interned_id = if let Some(interned_id) = + interned_ids_by_place_state.get(&place_state) + { + *interned_id + } else { + let bindings_id = if let Some(bindings_id) = + interned_ids_by_bindings.get(place_state.bindings()) + { + *bindings_id + } else { + let bindings_id = interned_bindings.push(place_state.bindings().clone()); + interned_ids_by_bindings.insert(place_state.bindings().clone(), bindings_id); + bindings_id + }; + let declarations_id = if let Some(declarations_id) = + interned_ids_by_declarations.get(place_state.declarations()) + { + *declarations_id + } else { + let declarations_id = + interned_declarations.push(place_state.declarations().clone()); + interned_ids_by_declarations + .insert(place_state.declarations().clone(), declarations_id); + declarations_id + }; + let place_state_id = InternedPlaceStateId(bindings_id, declarations_id); + interned_ids_by_place_state.insert(place_state, place_state_id); + place_state_id + }; + interned_ids_by_member.push(interned_id); + } + + interned_ids_by_member.shrink_to_fit(); + interned_ids_by_member + } + + fn intern_enclosing_snapshots( + enclosing_snapshots: EnclosingSnapshots, + interned_bindings: &mut IndexVec, + interned_ids_by_bindings: &mut FxHashMap, + ) -> IndexVec { + let mut interned_ids_by_snapshot: IndexVec< + ScopedEnclosingSnapshotId, + InternedEnclosingSnapshotId, + > = IndexVec::with_capacity(enclosing_snapshots.len()); + + for snapshot in enclosing_snapshots { + let interned_id = match snapshot { + EnclosingSnapshot::Bindings(bindings) => { + let interned_bindings_id = + if let Some(interned_id) = interned_ids_by_bindings.get(&bindings) { + *interned_id + } else { + let interned_id = interned_bindings.push(bindings.clone()); + interned_ids_by_bindings.insert(bindings, interned_id); + interned_id + }; + InternedEnclosingSnapshotId::Bindings(interned_bindings_id) + } + EnclosingSnapshot::Constraint(constraint) => { + InternedEnclosingSnapshotId::Constraint(constraint) + } + }; + interned_ids_by_snapshot.push(interned_id); + } + + interned_ids_by_snapshot.shrink_to_fit(); + interned_ids_by_snapshot + } } diff --git a/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs b/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs index 71833f3406397..cddde912af145 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs @@ -71,14 +71,14 @@ impl ScopedDefinitionId { /// Live declarations for a single place at some point in control flow, with their /// corresponding reachability constraints. -#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(super) struct Declarations { /// A list of live declarations for this place, sorted by their `ScopedDefinitionId` live_declarations: SmallVec<[LiveDeclaration; 2]>, } /// One of the live declarations for a single place at some point in control flow. -#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] pub(super) struct LiveDeclaration { pub(super) declaration: ScopedDefinitionId, pub(super) reachability_constraint: ScopedReachabilityConstraintId, @@ -184,28 +184,15 @@ impl Declarations { /// Even if it's a class scope (class variables are not visible to nested scopes) or there are no /// bindings, the current narrowing constraint is necessary for narrowing, so it's stored in /// `Constraint`. -#[derive(Clone, Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(super) enum EnclosingSnapshot { Constraint(ScopedNarrowingConstraint), Bindings(Bindings), } -impl EnclosingSnapshot { - pub(super) fn finish(&mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder) { - match self { - Self::Constraint(constraint) => { - reachability_constraints.mark_used(*constraint); - } - Self::Bindings(bindings) => { - bindings.finish(reachability_constraints); - } - } - } -} - /// Live bindings for a single place at some point in control flow. Each live binding comes /// with a set of narrowing constraints and a reachability constraint. -#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(super) struct Bindings { /// The narrowing constraint applicable to the "unbound" binding, if we need access to it even /// when it's not visible. This happens in class scopes, where local name bindings are not visible @@ -232,7 +219,7 @@ impl Bindings { } /// One of the live bindings for a single place at some point in control flow. -#[derive(Clone, Copy, Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(crate) struct LiveBinding { pub(crate) binding: ScopedDefinitionId, pub(crate) narrowing_constraint: ScopedNarrowingConstraint, @@ -358,7 +345,7 @@ impl Bindings { } } -#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] pub(in crate::semantic_index) struct PlaceState { declarations: Declarations, bindings: Bindings, From 03af8b27367e308cbcb5947a89a1c2af8185ccb3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 27 Feb 2026 21:48:14 +0000 Subject: [PATCH 129/261] [ty] Reject ellipsis literals in odd places in type/annotation expressions (#23611) --- .../resources/mdtest/annotations/callable.md | 15 +++ .../mdtest/generics/pep695/aliases.md | 25 ++-- .../types/infer/builder/type_expression.rs | 120 ++++++++++++------ 3 files changed, 112 insertions(+), 48 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index e201182c9ed9b..55d1377621b96 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -59,6 +59,21 @@ def _(c: Callable[[int, 42, str, False], None]): reveal_type(c) ``` +Or, when an ellipsis literal is used as a parameter type in the list (note that the valid gradual +form uses `...` as the entire first argument, not inside a list): + +```py +# error: [invalid-type-form] "`[...]` is not a valid parameter list for `Callable`: Did you mean `Callable[..., int]`?" +def _(c: Callable[[...], int]): + reveal_type(c) # revealed: (...) -> int +``` + +```py +# error: [invalid-type-form] "`...` is not allowed in this context in a type expression" +def _(c: Callable[[int, ...], int]): + reveal_type(c) # revealed: (int, Unknown, /) -> int +``` + ### Missing return type Using a parameter list: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 0df6c1da64d93..033452937f671 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -11,14 +11,15 @@ At its simplest, to define a type alias using PEP 695 syntax, you add a list of `ParamSpec`s or `TypeVarTuple`s after the alias name. ```py +from typing import Callable from ty_extensions import generic_context -type SingleTypevar[T] = ... -type MultipleTypevars[T, S] = ... -type SingleParamSpec[**P] = ... -type TypeVarAndParamSpec[T, **P] = ... -type SingleTypeVarTuple[*Ts] = ... -type TypeVarAndTypeVarTuple[T, *Ts] = ... +type SingleTypevar[T] = list[T] +type MultipleTypevars[T, S] = tuple[T, S] +type SingleParamSpec[**P] = Callable[P, int] +type TypeVarAndParamSpec[T, **P] = Callable[P, T] +type SingleTypeVarTuple[*Ts] = tuple[*Ts] +type TypeVarAndTypeVarTuple[T, *Ts] = tuple[T, *Ts] # revealed: ty_extensions.GenericContext[T@SingleTypevar] reveal_type(generic_context(SingleTypevar)) @@ -41,7 +42,7 @@ You cannot use the same typevar more than once. ```py # error: [invalid-syntax] "duplicate type parameter" -type RepeatedTypevar[T, T] = ... +type RepeatedTypevar[T, T] = tuple[T, T] ``` ## Specializing type aliases explicitly @@ -70,7 +71,7 @@ And non-generic types cannot be specialized: ```py from typing import TypeVar, Protocol, TypedDict -type B = ... +type B = int # error: [not-subscriptable] "Cannot subscript non-generic type alias `B`" reveal_type(B[int]) # revealed: Unknown @@ -158,8 +159,8 @@ def _(x: Union[int]): If the type variable has an upper bound, the specialized type must satisfy that bound: ```py -type Bounded[T: int] = ... -type BoundedByUnion[T: int | str] = ... +type Bounded[T: int] = list[T] +type BoundedByUnion[T: int | str] = list[T] class IntSubclass(int): ... @@ -190,7 +191,7 @@ def _(x: TupleOfIntAndStr[int, int]): If the type variable is constrained, the specialized type must satisfy those constraints: ```py -type Constrained[T: (int, str)] = ... +type Constrained[T: (int, str)] = list[T] reveal_type(Constrained[int]) # revealed: @@ -220,7 +221,7 @@ def _(x: TupleOfIntOrStr[int, object]): If the type variable has a default, it can be omitted: ```py -type WithDefault[T, U = int] = ... +type WithDefault[T, U = int] = dict[T, U] reveal_type(WithDefault[str, str]) # revealed: reveal_type(WithDefault[str]) # revealed: diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index d68ec157ec724..cdffa66228759 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -65,7 +65,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fn report_invalid_type_expression( &self, expression: &ast::Expr, - message: std::fmt::Arguments, + message: impl std::fmt::Display, ) -> Option> { self.context .report_lint(&INVALID_TYPE_FORM, expression) @@ -514,7 +514,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Expr::IpyEscapeCommand(_) => todo!("Implement Ipy escape command support"), ast::Expr::EllipsisLiteral(_) => { - todo_type!("ellipsis literal in type expression") + self.report_invalid_type_expression( + expression, + "`...` is not allowed in this context in a type expression", + ); + Type::unknown() } ast::Expr::Starred(starred) => self.infer_starred_type_expression(starred), @@ -639,15 +643,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut first_unpacked_variadic_tuple = None; for element in elements { - if element.is_ellipsis_literal_expr() - && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) - { - let mut diagnostic = - builder.into_diagnostic("Invalid `tuple` specialization"); - diagnostic.set_primary_message( - "`...` can only be used as the second element \ - in a two-element `tuple` specialization", - ); + if element.is_ellipsis_literal_expr() { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) { + let mut diagnostic = + builder.into_diagnostic("Invalid `tuple` specialization"); + diagnostic.set_primary_message( + "`...` can only be used as the second element \ + in a two-element `tuple` specialization", + ); + } + self.store_expression_type(element, Type::unknown()); + element_types.push(Type::unknown()); + continue; } let element_ty = self.infer_type_expression(element); return_todo |= @@ -720,14 +727,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ty } single_element => { - if single_element.is_ellipsis_literal_expr() - && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) - { - let mut diagnostic = builder.into_diagnostic("Invalid `tuple` specialization"); - diagnostic.set_primary_message( - "`...` can only be used as the second element \ - in a two-element `tuple` specialization", - ); + if single_element.is_ellipsis_literal_expr() { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) { + let mut diagnostic = + builder.into_diagnostic("Invalid `tuple` specialization"); + diagnostic.set_primary_message( + "`...` can only be used as the second element \ + in a two-element `tuple` specialization", + ); + } + self.store_expression_type(single_element, Type::unknown()); + return TupleType::heterogeneous(self.db(), std::iter::once(Type::unknown())); } let single_element_ty = self.infer_type_expression(single_element); if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self) @@ -1282,26 +1292,53 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let return_type = arguments.next().map(|arg| self.infer_type_expression(arg)); - let correct_argument_number = if let Some(third_argument) = arguments.next() { - self.infer_type_expression(third_argument); - for argument in arguments { - self.infer_type_expression(argument); + let callable_type = if parameters.is_none() + && let Some(first_argument) = first_argument + && let ast::Expr::List(list) = first_argument + && let [single_param] = &list.elts[..] + && single_param.is_ellipsis_literal_expr() + { + self.store_expression_type(single_param, Type::unknown()); + if let Some(mut diagnostic) = self.report_invalid_type_expression( + first_argument, + "`[...]` is not a valid parameter list for `Callable`", + ) { + if let Some(returns) = return_type { + diagnostic.set_primary_message(format_args!( + "Did you mean `Callable[..., {}]`?", + returns.display(db) + )); + } } - false + Type::single_callable( + db, + Signature::new( + Parameters::unknown(), + return_type.unwrap_or_else(Type::unknown), + ), + ) } else { - return_type.is_some() - }; + let correct_argument_number = if let Some(third_argument) = arguments.next() { + self.infer_type_expression(third_argument); + for argument in arguments { + self.infer_type_expression(argument); + } + false + } else { + return_type.is_some() + }; - if !correct_argument_number { - report_invalid_arguments_to_callable(&self.context, subscript); - } + if !correct_argument_number { + report_invalid_arguments_to_callable(&self.context, subscript); + } - let callable_type = if let (Some(parameters), Some(return_type), true) = - (parameters, return_type, correct_argument_number) - { - Type::single_callable(db, Signature::new(parameters, return_type)) - } else { - Type::Callable(CallableType::unknown(db)) + if correct_argument_number + && let (Some(parameters), Some(return_type)) = (parameters, return_type) + { + Type::single_callable(db, Signature::new(parameters, return_type)) + } else { + Type::Callable(CallableType::unknown(db)) + } }; // `Signature` / `Parameters` are not a `Type` variant, so we're storing @@ -1610,7 +1647,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { std::slice::from_ref(arguments_slice) }; for argument in arguments { - self.infer_type_expression(argument); + if argument.is_ellipsis_literal_expr() { + // The trailing `...` in `Concatenate[int, str, ...]` is valid; + // store without going through type-expression inference. + self.store_expression_type(argument, Type::unknown()); + } else { + self.infer_type_expression(argument); + } } let num_arguments = arguments.len(); let inferred_type = if num_arguments < 2 { @@ -1847,6 +1890,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Some(Parameters::gradual_form()); } ast::Expr::List(ast::ExprList { elts: params, .. }) => { + if let [ast::Expr::EllipsisLiteral(_)] = ¶ms[..] { + // Return `None` here so that we emit a specific diagnostic at the callsite. + return None; + } + let mut parameter_types = Vec::with_capacity(params.len()); // Whether to infer `Todo` for the parameters From 555792d2e85166f02634c37ae59f9d0d03aa3148 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 27 Feb 2026 22:07:23 +0000 Subject: [PATCH 130/261] [ty] Reject functions with PEP-695 type parameters that shadow type parameters from enclosing scopes (#23619) --- crates/ty/docs/rules.md | 232 ++++++++++-------- .../resources/mdtest/function/return_type.md | 2 +- .../mdtest/generics/legacy/classes.md | 4 +- .../mdtest/generics/pep695/classes.md | 2 +- .../mdtest/generics/pep695/functions.md | 2 +- .../resources/mdtest/generics/scoping.md | 18 +- ...ithin\342\200\246_(3259718bf20b45a2).snap" | 25 +- ...ithin\342\200\246_(711fb86287c4d87b).snap" | 25 +- ...n_wit\342\200\246_(f58a51442a16371e).snap" | 42 ++++ ...withi\342\200\246_(c19e9277cf9fafb5).snap" | 42 ++++ .../src/types/diagnostic.rs | 45 +++- .../src/types/infer/builder.rs | 41 +++- ty.schema.json | 10 + 13 files changed, 340 insertions(+), 150 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 759e5754ef232..1788d7b7558a1 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -640,7 +640,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -672,7 +672,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -798,7 +798,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -828,7 +828,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -854,7 +854,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -888,7 +888,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -977,7 +977,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1004,7 +1004,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1032,7 +1032,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1066,7 +1066,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1102,7 +1102,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1126,7 +1126,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1153,7 +1153,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1190,7 +1190,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1222,7 +1222,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1251,7 +1251,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1295,7 +1295,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1337,7 +1337,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1381,7 +1381,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1419,7 +1419,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1498,7 +1498,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1537,7 +1537,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1598,7 +1598,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1633,7 +1633,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1661,7 +1661,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1695,7 +1695,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1802,7 +1802,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1856,7 +1856,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1886,7 +1886,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1936,7 +1936,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1962,7 +1962,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1993,7 +1993,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2027,7 +2027,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2076,7 +2076,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2105,7 +2105,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2201,7 +2201,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2247,7 +2247,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2274,7 +2274,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2321,7 +2321,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2351,7 +2351,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2381,7 +2381,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2415,7 +2415,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2449,7 +2449,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2480,7 +2480,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2527,7 +2527,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2559,7 +2559,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2594,7 +2594,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2625,7 +2625,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2680,7 +2680,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2723,7 +2723,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2748,7 +2748,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2781,7 +2781,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2810,7 +2810,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2836,7 +2836,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2860,7 +2860,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2893,7 +2893,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2926,7 +2926,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2953,7 +2953,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2980,7 +2980,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3008,7 +3008,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3040,7 +3040,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3077,7 +3077,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3141,7 +3141,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3168,7 +3168,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3194,13 +3194,47 @@ class C: y: Final[ClassVar[int]] = 1 # redundant ``` +## `shadowed-type-variable` + + +Default level: error · +Added in 0.0.20 · +Related issues · +View source + + + +**What it does** + +Checks for type variables in nested generic classes or functions that shadow type variables +from an enclosing scope. + +**Why is this bad?** + +Shadowing type variables makes the code confusing and is disallowed by the typing spec. + +**Examples** + +```python +class Outer[T]: + # Error: `T` is already used by `Outer` + class Inner[T]: ... + + # Error: `T` is already used by `Outer` + def method[T](self, x: T) -> T: ... +``` + +**References** + +- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) + ## `static-assert-error` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3230,7 +3264,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3259,7 +3293,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3293,7 +3327,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3320,7 +3354,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3348,7 +3382,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3394,7 +3428,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3418,7 +3452,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3445,7 +3479,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3473,7 +3507,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3531,7 +3565,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3556,7 +3590,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3581,7 +3615,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3620,7 +3654,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3657,7 +3691,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3698,7 +3732,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3799,7 +3833,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3862,7 +3896,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/function/return_type.md b/crates/ty_python_semantic/resources/mdtest/function/return_type.md index 61fa6480db95b..6d399ed07dd66 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/return_type.md +++ b/crates/ty_python_semantic/resources/mdtest/function/return_type.md @@ -105,7 +105,7 @@ class Bar[T](ABC): @abstractmethod def f(self) -> int: ... @abstractmethod - def g[T](self, x: T) -> T: ... + def g[U](self, x: U) -> U: ... # error: [empty-body] def f() -> int: ... diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 494c57f41ed4e..8c9774d18b070 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -88,13 +88,13 @@ present, they are not included in the class's generic context. ```py class OuterClass(Generic[T]): - # error: [invalid-generic-class] "Generic class `InnerClass` must not reference type variables bound in an enclosing scope" + # error: [shadowed-type-variable] "Generic class `InnerClass` uses type variable `T` already bound by an enclosing scope" class InnerClass(list[T]): ... # revealed: None reveal_type(generic_context(InnerClass)) def method(self): - # error: [invalid-generic-class] "Generic class `InnerClassInMethod` must not reference type variables bound in an enclosing scope" + # error: [shadowed-type-variable] "Generic class `InnerClassInMethod` uses type variable `T` already bound by an enclosing scope" class InnerClassInMethod(list[T]): ... # revealed: None reveal_type(generic_context(InnerClassInMethod)) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index d74a8c8c2d447..1d26718721197 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -659,7 +659,7 @@ class C[T]: # error: [unresolved-reference] def cannot_use_outside_of_method(self, u: U): ... - # TODO: error + # error: [shadowed-type-variable] def cannot_shadow_class_typevar[T](self, t: T): ... # revealed: ty_extensions.GenericContext[T@C] diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 70197b91af271..57eb1184a0a34 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -689,7 +689,7 @@ def test[T: int](items: list[T]) -> list[T]: from typing import overload def outer[T](t: T) -> None: - def inner[T](t: T) -> None: ... + def inner[T](t: T) -> None: ... # error: [shadowed-type-variable] inner(t) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index 32c31882d18be..21eca20740bd0 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -242,21 +242,25 @@ We assume that the more general form holds. ### Generic function within generic function + + ```py def f[T](x: T, y: T) -> None: def ok[S](a: S, b: S) -> None: ... - # TODO: error + # error: [shadowed-type-variable] def bad[T](a: T, b: T) -> None: ... ``` ### Generic method within generic class + + ```py class C[T]: def ok[S](self, a: S, b: S) -> None: ... - # TODO: error + # error: [shadowed-type-variable] def bad[T](self, a: T, b: T) -> None: ... ``` @@ -269,9 +273,9 @@ from typing import Iterable def f[T](x: T, y: T) -> None: class Ok[S]: ... - # error: [invalid-generic-class] + # error: [shadowed-type-variable] class Bad1[T]: ... - # error: [invalid-generic-class] + # error: [shadowed-type-variable] class Bad2(Iterable[T]): ... ``` @@ -284,9 +288,9 @@ from typing import Iterable class C[T]: class Ok1[S]: ... - # error: [invalid-generic-class] + # error: [shadowed-type-variable] class Bad1[T]: ... - # error: [invalid-generic-class] + # error: [shadowed-type-variable] class Bad2(Iterable[T]): ... ``` @@ -310,7 +314,7 @@ list: ```py class Outer[T]: - # error: [invalid-generic-class] + # error: [shadowed-type-variable] class Bad(list[T]): ... ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" index 1774bf0a1c193..478268091b0ae 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" @@ -1,5 +1,6 @@ --- source: crates/ty_test/src/lib.rs +assertion_line: 624 expression: snapshot --- @@ -17,16 +18,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md 2 | 3 | def f[T](x: T, y: T) -> None: 4 | class Ok[S]: ... -5 | # error: [invalid-generic-class] +5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... -7 | # error: [invalid-generic-class] +7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... ``` # Diagnostics ``` -error[invalid-generic-class]: Generic class `Bad1` must not reference type variables bound in an enclosing scope +error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` already bound by an enclosing scope --> src/mdtest_snippet.py:3:5 | 1 | from typing import Iterable @@ -34,18 +35,18 @@ error[invalid-generic-class]: Generic class `Bad1` must not reference type varia 3 | def f[T](x: T, y: T) -> None: | ------------------------ Type variable `T` is bound in this enclosing scope 4 | class Ok[S]: ... -5 | # error: [invalid-generic-class] +5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... - | ^^^^ `T` referenced in class definition here -7 | # error: [invalid-generic-class] + | ^^^^ `T` used in class definition here +7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... | -info: rule `invalid-generic-class` is enabled by default +info: rule `shadowed-type-variable` is enabled by default ``` ``` -error[invalid-generic-class]: Generic class `Bad2` must not reference type variables bound in an enclosing scope +error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` already bound by an enclosing scope --> src/mdtest_snippet.py:3:5 | 1 | from typing import Iterable @@ -53,12 +54,12 @@ error[invalid-generic-class]: Generic class `Bad2` must not reference type varia 3 | def f[T](x: T, y: T) -> None: | ------------------------ Type variable `T` is bound in this enclosing scope 4 | class Ok[S]: ... -5 | # error: [invalid-generic-class] +5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... -7 | # error: [invalid-generic-class] +7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... - | ^^^^^^^^^^^^^^^^^ `T` referenced in class definition here + | ^^^^^^^^^^^^^^^^^ `T` used in class definition here | -info: rule `invalid-generic-class` is enabled by default +info: rule `shadowed-type-variable` is enabled by default ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" index 36c097f4a14d8..bf823b7861094 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" @@ -1,5 +1,6 @@ --- source: crates/ty_test/src/lib.rs +assertion_line: 624 expression: snapshot --- @@ -17,16 +18,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md 2 | 3 | class C[T]: 4 | class Ok1[S]: ... -5 | # error: [invalid-generic-class] +5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... -7 | # error: [invalid-generic-class] +7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... ``` # Diagnostics ``` -error[invalid-generic-class]: Generic class `Bad1` must not reference type variables bound in an enclosing scope +error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` already bound by an enclosing scope --> src/mdtest_snippet.py:3:7 | 1 | from typing import Iterable @@ -34,18 +35,18 @@ error[invalid-generic-class]: Generic class `Bad1` must not reference type varia 3 | class C[T]: | - Type variable `T` is bound in this enclosing scope 4 | class Ok1[S]: ... -5 | # error: [invalid-generic-class] +5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... - | ^^^^ `T` referenced in class definition here -7 | # error: [invalid-generic-class] + | ^^^^ `T` used in class definition here +7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... | -info: rule `invalid-generic-class` is enabled by default +info: rule `shadowed-type-variable` is enabled by default ``` ``` -error[invalid-generic-class]: Generic class `Bad2` must not reference type variables bound in an enclosing scope +error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` already bound by an enclosing scope --> src/mdtest_snippet.py:3:7 | 1 | from typing import Iterable @@ -53,12 +54,12 @@ error[invalid-generic-class]: Generic class `Bad2` must not reference type varia 3 | class C[T]: | - Type variable `T` is bound in this enclosing scope 4 | class Ok1[S]: ... -5 | # error: [invalid-generic-class] +5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... -7 | # error: [invalid-generic-class] +7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... - | ^^^^^^^^^^^^^^^^^ `T` referenced in class definition here + | ^^^^^^^^^^^^^^^^^ `T` used in class definition here | -info: rule `invalid-generic-class` is enabled by default +info: rule `shadowed-type-variable` is enabled by default ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" new file mode 100644 index 0000000000000..3646c6bff4e55 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" @@ -0,0 +1,42 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 624 +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Nested formal typevars must be distinct - Generic function within generic function +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def f[T](x: T, y: T) -> None: +2 | def ok[S](a: S, b: S) -> None: ... +3 | +4 | # error: [shadowed-type-variable] +5 | def bad[T](a: T, b: T) -> None: ... +``` + +# Diagnostics + +``` +error[shadowed-type-variable]: Generic function `bad` uses type variable `T` already bound by an enclosing scope + --> src/mdtest_snippet.py:5:9 + | +4 | # error: [shadowed-type-variable] +5 | def bad[T](a: T, b: T) -> None: ... + | ^^^ `T` used in function definition here + | + ::: src/mdtest_snippet.py:1:5 + | +1 | def f[T](x: T, y: T) -> None: + | ------------------------ Type variable `T` is bound in this enclosing scope +2 | def ok[S](a: S, b: S) -> None: ... + | +info: rule `shadowed-type-variable` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" new file mode 100644 index 0000000000000..39b648f1c9269 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" @@ -0,0 +1,42 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 624 +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Nested formal typevars must be distinct - Generic method within generic class +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C[T]: +2 | def ok[S](self, a: S, b: S) -> None: ... +3 | +4 | # error: [shadowed-type-variable] +5 | def bad[T](self, a: T, b: T) -> None: ... +``` + +# Diagnostics + +``` +error[shadowed-type-variable]: Generic function `bad` uses type variable `T` already bound by an enclosing scope + --> src/mdtest_snippet.py:5:9 + | +4 | # error: [shadowed-type-variable] +5 | def bad[T](self, a: T, b: T) -> None: ... + | ^^^ `T` used in function definition here + | + ::: src/mdtest_snippet.py:1:7 + | +1 | class C[T]: + | - Type variable `T` is bound in this enclosing scope +2 | def ok[S](self, a: S, b: S) -> None: ... + | +info: rule `shadowed-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 0485f2fad1d40..3256a7a2cf1c3 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -115,6 +115,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&POSSIBLY_MISSING_ATTRIBUTE); registry.register_lint(&POSSIBLY_MISSING_IMPORT); registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE); + registry.register_lint(&SHADOWED_TYPE_VARIABLE); registry.register_lint(&SUBCLASS_OF_FINAL_CLASS); registry.register_lint(&OVERRIDE_OF_FINAL_METHOD); registry.register_lint(&OVERRIDE_OF_FINAL_VARIABLE); @@ -2729,6 +2730,33 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for type variables in nested generic classes or functions that shadow type variables + /// from an enclosing scope. + /// + /// ## Why is this bad? + /// Shadowing type variables makes the code confusing and is disallowed by the typing spec. + /// + /// ## Examples + /// ```python + /// class Outer[T]: + /// # Error: `T` is already used by `Outer` + /// class Inner[T]: ... + /// + /// # Error: `T` is already used by `Outer` + /// def method[T](self, x: T) -> T: ... + /// ``` + /// + /// ## References + /// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) + pub(crate) static SHADOWED_TYPE_VARIABLE = { + summary: "detects type variables that shadow type variables from outer scopes", + status: LintStatus::stable("0.0.20"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Detects variables declared as `global` in an inner scope that have no explicit @@ -4914,23 +4942,26 @@ pub(crate) fn report_invalid_type_param_order<'db>( } } -pub(crate) fn report_rebound_typevar<'db>( +pub(crate) fn report_shadowed_type_variable<'db>( context: &InferContext<'db, '_>, typevar_name: &ast::name::Name, - class: StaticClassLiteral<'db>, - class_node: &ast::StmtClassDef, + kind: &str, + name: &ast::name::Name, + range: TextRange, other_typevar: BoundTypeVarInstance<'db>, ) { let db = context.db(); - let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, class.header_range(db)) else { + let Some(builder) = context.report_lint(&SHADOWED_TYPE_VARIABLE, range) else { return; }; let mut diagnostic = builder.into_diagnostic(format_args!( - "Generic class `{}` must not reference type variables bound in an enclosing scope", - class_node.name, + "Generic {kind} `{name}` uses type variable `{typevar_name}` already bound by an enclosing scope", + )); + diagnostic.set_concise_message(format_args!( + "Generic {kind} `{name}` uses type variable `{typevar_name}` already bound by an enclosing scope", )); diagnostic.set_primary_message(format_args!( - "`{typevar_name}` referenced in class definition here" + "`{typevar_name}` used in {kind} definition here" )); let Some(other_definition) = other_typevar.binding_context(db).definition() else { return; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index a9ce47c3049cf..0ecb5e38ee4ac 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -105,8 +105,8 @@ use crate::types::diagnostic::{ report_match_pattern_against_typed_dict, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, - report_rebound_typevar, report_unsupported_augmented_assignment, report_unsupported_base, - report_unsupported_binary_operation, report_unsupported_comparison, + report_shadowed_type_variable, report_unsupported_augmented_assignment, + report_unsupported_base, report_unsupported_binary_operation, report_unsupported_comparison, }; use crate::types::enums::is_enum_class_by_inheritance; use crate::types::function::{ @@ -1452,11 +1452,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(other_typevar) = enclosing.binds_named_typevar(self.db(), name) { - report_rebound_typevar( + report_shadowed_type_variable( &self.context, name, - class, - class_node, + "class", + &class_node.name.id, + class.header_range(self.db()), other_typevar, ); } @@ -1471,11 +1472,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) { if let Some(other_typevar) = enclosing.binds_typevar(self.db(), typevar) { - report_rebound_typevar( + report_shadowed_type_variable( &self.context, typevar.name(self.db()), - class, - class_node, + "class", + &class_node.name.id, + class.header_range(self.db()), other_typevar, ); } @@ -3284,6 +3286,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::FunctionLiteral(FunctionType::new(self.db(), function_literal, None, None)); self.undecorated_type = Some(inferred_ty); + // Check that the function's own type parameters don't shadow + // type variables from enclosing scopes (by name). + if let Some(type_params) = &function.type_params { + let current_scope = self.scope().file_scope_id(self.db()); + for type_param in type_params.iter() { + let param_name = type_param.name(); + for enclosing in enclosing_generic_contexts(self.db(), self.index, current_scope) { + if let Some(other_typevar) = + enclosing.binds_named_typevar(self.db(), ¶m_name.id) + { + report_shadowed_type_variable( + &self.context, + ¶m_name.id, + "function", + &function.name.id, + function.name.range(), + other_typevar, + ); + } + } + } + } + for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { inferred_ty = self.apply_decorator(*decorator_ty, inferred_ty, decorator_node); } diff --git a/ty.schema.json b/ty.schema.json index ff954b6d68d2a..67193ebf9b9d1 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1255,6 +1255,16 @@ } ] }, + "shadowed-type-variable": { + "title": "detects type variables that shadow type variables from outer scopes", + "description": "## What it does\nChecks for type variables in nested generic classes or functions that shadow type variables\nfrom an enclosing scope.\n\n## Why is this bad?\nShadowing type variables makes the code confusing and is disallowed by the typing spec.\n\n## Examples\n```python\nclass Outer[T]:\n # Error: `T` is already used by `Outer`\n class Inner[T]: ...\n\n # Error: `T` is already used by `Outer`\n def method[T](self, x: T) -> T: ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "static-assert-error": { "title": "Failed static assertion", "description": "## What it does\nMakes sure that the argument of `static_assert` is statically known to be true.\n\n## Why is this bad?\nA `static_assert` call represents an explicit request from the user\nfor the type checker to emit an error if the argument cannot be verified\nto evaluate to `True` in a boolean context.\n\n## Examples\n```python\nfrom ty_extensions import static_assert\n\nstatic_assert(1 + 1 == 3) # error: evaluates to `False`\n\nstatic_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness\n```", From cb2035aecaf68d8d298fc78cb20d7119f5268a4b Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 27 Feb 2026 14:24:52 -0800 Subject: [PATCH 131/261] fix binops with NewType of float and Unknown (#23620) ## Summary Fixes https://github.com/astral-sh/ty/issues/2914 Binops with dynamic types have to result in dynamic types. Move this case up so it takes precedence over some NewType and TypeVar special cases. ## Test Plan Added mdtest. --- .../resources/mdtest/annotations/new_types.md | 14 ++++++++ .../src/types/infer/builder.rs | 34 +++++++++---------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index 03f3ca3817e36..55ea8e59f1513 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -352,6 +352,20 @@ Bing() < 3.14 # error: [unsupported-operator] 3.14 in Bing() # error: [unsupported-operator] ``` +`Unknown` should still propagate through these operations. + +```py +from typing import NewType +from doesnotexist import unknown # error: [unresolved-import] + +MyFloat = NewType("MyFloat", float) + +reveal_type(unknown) # revealed: Unknown +reveal_type(1.0 * unknown) # revealed: Unknown +reveal_type(MyFloat(1.0) * unknown) # revealed: Unknown +reveal_type(unknown * MyFloat(1.0)) # revealed: Unknown +``` + Unary operations take a different codepath and need their own test cases: ```py diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 0ecb5e38ee4ac..9d1c6670c624d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -14301,6 +14301,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { op, ), + // Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future, + // the result would then become Any or Unknown, respectively). + (div @ Type::Dynamic(DynamicType::Divergent(_)), _, _) + | (_, div @ Type::Dynamic(DynamicType::Divergent(_)), _) => Some(div), + + (any @ Type::Dynamic(DynamicType::Any), _, _) + | (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any), + + (unknown @ Type::Dynamic(DynamicType::Unknown), _, _) + | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), + + (unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _, _) + | (_, unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _) => Some(unknown), + + (typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _, _) + | (_, typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _) => Some(typevar), + // When both operands are the same constrained TypeVar (e.g., `T: (int, str)`), // we check if the operation is valid for each constraint paired with itself. // This is different from treating it as a union, where we'd check all combinations. @@ -14437,23 +14454,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) } - // Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future, - // the result would then become Any or Unknown, respectively). - (div @ Type::Dynamic(DynamicType::Divergent(_)), _, _) - | (_, div @ Type::Dynamic(DynamicType::Divergent(_)), _) => Some(div), - - (any @ Type::Dynamic(DynamicType::Any), _, _) - | (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any), - - (unknown @ Type::Dynamic(DynamicType::Unknown), _, _) - | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), - - (unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _, _) - | (_, unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _) => Some(unknown), - - (typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _, _) - | (_, typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _) => Some(typevar), - ( todo @ Type::Dynamic( DynamicType::Todo(_) From bc12fbb565b92df8ee24ad4328c9f783c26e16f4 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:00:03 -0500 Subject: [PATCH 132/261] Update default Python version examples (#23605) ## Summary Brings our documentation up to date with the implementation, see https://github.com/astral-sh/ruff/pull/17529#issuecomment-3972000021 ## Test Plan --- README.md | 4 ++-- docs/configuration.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a46518c4b019e..3d0be7e8ec40b 100644 --- a/README.md +++ b/README.md @@ -254,8 +254,8 @@ exclude = [ line-length = 88 indent-width = 4 -# Assume Python 3.9 -target-version = "py39" +# Assume Python 3.10 +target-version = "py310" [lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. diff --git a/docs/configuration.md b/docs/configuration.md index 08049f5f1396b..f015a8071b5e8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -47,8 +47,8 @@ If left unspecified, Ruff's default configuration is equivalent to: line-length = 88 indent-width = 4 - # Assume Python 3.9 - target-version = "py39" + # Assume Python 3.10 + target-version = "py310" [tool.ruff.lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. @@ -129,8 +129,8 @@ If left unspecified, Ruff's default configuration is equivalent to: line-length = 88 indent-width = 4 - # Assume Python 3.9 - target-version = "py39" + # Assume Python 3.10 + target-version = "py310" [lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. From f1a9a703e1992520d7edd3d907d2ed6a019a120b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 28 Feb 2026 02:48:36 +0000 Subject: [PATCH 133/261] [ty] Reject generic metaclasses parameterized by type variables (#23628) ## Summary Add validation to reject generic metaclasses that are parameterized by type variables, as these are banned by the typing spec. ## Test Plan mdtests --------- Co-authored-by: Claude --- .../resources/mdtest/metaclass.md | 52 +++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 19 +++++++ .../src/types/infer/builder.rs | 7 +++ 3 files changed, 78 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/metaclass.md b/crates/ty_python_semantic/resources/mdtest/metaclass.md index 3c4762fe8fefe..2a05f779f1061 100644 --- a/crates/ty_python_semantic/resources/mdtest/metaclass.md +++ b/crates/ty_python_semantic/resources/mdtest/metaclass.md @@ -247,6 +247,58 @@ class A[T: str](metaclass=M): ... reveal_type(A.__class__) # revealed: ``` +## Generic metaclass + +### Fully specialized + +A generic metaclass fully specialized with concrete types is fine: + +```toml +[environment] +python-version = "3.13" +``` + +```py +class Foo[T](type): + x: T + +class Bar(metaclass=Foo[int]): ... + +reveal_type(Bar.__class__) # revealed: +``` + +### Parameterized by type variables (legacy) + +A generic metaclass parameterized by type variables is not supported: + +```py +from typing import TypeVar, Generic + +T = TypeVar("T") + +class GenericMeta(type, Generic[T]): ... + +# error: [invalid-metaclass] "Generic metaclasses are not supported" +class GenericMetaInstance(metaclass=GenericMeta[T]): ... +``` + +### Parameterized by type variables (PEP 695) + +The same applies using PEP 695 syntax: + +```toml +[environment] +python-version = "3.13" +``` + +```py +class Foo[T](type): + x: T + +# error: [invalid-metaclass] +class Bar[T](metaclass=Foo[T]): ... +``` + ## Metaclasses of metaclasses ```py diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 2316040ea8878..6532df131e769 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2883,6 +2883,23 @@ impl<'db> StaticClassLiteral<'db> { let module = parsed_module(db, self.file(db)).load(db); let explicit_metaclass = self.explicit_metaclass(db, &module); + + // Generic metaclasses parameterized by type variables are not supported. + // `metaclass=Meta[int]` is fine, but `metaclass=Meta[T]` is not. + // See: https://typing.python.org/en/latest/spec/generics.html#generic-metaclasses + if let Some(Type::GenericAlias(alias)) = explicit_metaclass { + let specialization_has_typevars = alias + .specialization(db) + .types(db) + .iter() + .any(|ty| ty.has_typevar_or_typevar_instance(db)); + if specialization_has_typevars { + return Err(MetaclassError { + kind: MetaclassErrorKind::GenericMetaclass, + }); + } + } + let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { (metaclass, self) } else if let Some(base_class) = base_classes.next() { @@ -8276,6 +8293,8 @@ pub(super) enum MetaclassErrorKind<'db> { /// inferred metaclass of a base class. This helps us give better error messages in diagnostics. candidate1_is_base_class: bool, }, + /// The metaclass is a parameterized generic class, which is not supported. + GenericMetaclass, /// The metaclass is not callable NotCallable(Type<'db>), /// The metaclass is of a union type whose some members are not callable diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9d1c6670c624d..1d89d403771a3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1222,6 +1222,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); } } + MetaclassErrorKind::GenericMetaclass => { + if let Some(builder) = + self.context.report_lint(&INVALID_METACLASS, class_node) + { + builder.into_diagnostic("Generic metaclasses are not supported"); + } + } MetaclassErrorKind::NotCallable(ty) => { if let Some(builder) = self.context.report_lint(&INVALID_METACLASS, class_node) From aa13ddf86f1ebaf05144e5b2cf89234f2fb4f57c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 28 Feb 2026 14:24:11 +0000 Subject: [PATCH 134/261] [ty] Ban nested `Required`/`NotRequired`, and ban them both outside of `TypedDict` fields (#23627) --- .../resources/mdtest/typed_dict.md | 49 ++++++++++++++++++- crates/ty_python_semantic/src/types.rs | 17 ++++++- .../ty_python_semantic/src/types/call/bind.rs | 5 +- .../src/types/class_base.rs | 1 + .../src/types/infer/builder.rs | 42 +++++++++++++++- .../infer/builder/annotation_expression.rs | 17 +++++++ crates/ty_test/src/matcher.rs | 1 + 7 files changed, 125 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 9fa17eddf669b..c9d892e2e45df 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -1849,7 +1849,7 @@ msg = Message(id=1, content="Hello") # No errors for yet-unsupported features (`closed`): OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True) -reveal_type(Message.__required_keys__) # revealed: @Todo(Support for functional `TypedDict`) +reveal_type(Message.__required_keys__) # revealed: @Todo(Functional TypedDicts) # TODO: this should be an error msg.content @@ -1883,6 +1883,53 @@ def bad( ): ... ``` +### `Required` and `NotRequired` not allowed outside `TypedDict` + +```py +from typing_extensions import Required, NotRequired, TypedDict + +# error: [invalid-type-form] "`Required` is only allowed in TypedDict fields" +x: Required[int] +# error: [invalid-type-form] "`NotRequired` is only allowed in TypedDict fields" +y: NotRequired[str] + +class MyClass: + # error: [invalid-type-form] "`Required` is only allowed in TypedDict fields" + x: Required[int] + # error: [invalid-type-form] "`NotRequired` is only allowed in TypedDict fields" + y: NotRequired[str] + +def f(): + # error: [invalid-type-form] "`Required` is only allowed in TypedDict fields" + x: Required[int] = 1 + # error: [invalid-type-form] "`NotRequired` is only allowed in TypedDict fields" + y: NotRequired[str] = "" + +# fine +MyFunctionalTypedDict = TypedDict("MyFunctionalTypedDict", {"not-an-identifier": Required[int]}) + +class FunctionalTypedDictSubclass(MyFunctionalTypedDict): + y: NotRequired[int] # fine +``` + +### Nested `Required` and `NotRequired` + +`Required` and `NotRequired` cannot be nested inside each other: + +```py +from typing_extensions import TypedDict, Required, NotRequired + +class TD(TypedDict): + # error: [invalid-type-form] "`typing.Required` cannot be nested inside `Required` or `NotRequired`" + a: Required[Required[int]] + # error: [invalid-type-form] "`typing.NotRequired` cannot be nested inside `Required` or `NotRequired`" + b: NotRequired[NotRequired[int]] + # error: [invalid-type-form] "`typing.Required` cannot be nested inside `Required` or `NotRequired`" + c: Required[NotRequired[int]] + # error: [invalid-type-form] "`typing.NotRequired` cannot be nested inside `Required` or `NotRequired`" + d: NotRequired[Required[int]] +``` + ### `dict`-subclass inhabitants Values that inhabit a `TypedDict` type must be instances of `dict` itself, not a subclass: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 166e2a7bc2dda..9cedb38fb8a8f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -939,6 +939,7 @@ impl<'db> Type<'db> { DynamicType::Todo(_) | DynamicType::TodoStarredExpression | DynamicType::TodoUnpack + | DynamicType::TodoFunctionalTypedDict | DynamicType::TodoTypeVarTuple => true, }) } @@ -4221,7 +4222,7 @@ impl<'db> Type<'db> { .with_annotated_type(Type::any()), ], ), - Type::unknown(), + Type::Dynamic(DynamicType::TodoFunctionalTypedDict), ), ) .into() @@ -6817,7 +6818,15 @@ impl<'db> Type<'db> { Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db), // These types have no definition - Self::Dynamic(DynamicType::Divergent(_) | DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression | DynamicType::TodoTypeVarTuple | DynamicType::UnspecializedTypeVar) + Self::Dynamic( + DynamicType::Divergent(_) + | DynamicType::Todo(_) + | DynamicType::TodoUnpack + | DynamicType::TodoStarredExpression + | DynamicType::TodoTypeVarTuple + | DynamicType::UnspecializedTypeVar + | DynamicType::TodoFunctionalTypedDict + ) | Self::Callable(_) | Self::TypeIs(_) | Self::TypeGuard(_) => None, @@ -7538,6 +7547,8 @@ pub enum DynamicType<'db> { TodoStarredExpression, /// A special Todo-variant for `TypeVarTuple` instances encountered in type expressions TodoTypeVarTuple, + /// A special Todo-variant for functional `TypedDict`s. + TodoFunctionalTypedDict, /// A type that is determined to be divergent during recursive type inference. Divergent(DivergentType), } @@ -7564,6 +7575,7 @@ impl std::fmt::Display for DynamicType<'_> { DynamicType::TodoUnpack => f.write_str("@Todo(typing.Unpack)"), DynamicType::TodoStarredExpression => f.write_str("@Todo(StarredExpression)"), DynamicType::TodoTypeVarTuple => f.write_str("@Todo(TypeVarTuple)"), + DynamicType::TodoFunctionalTypedDict => f.write_str("@Todo(Functional TypedDicts)"), DynamicType::Divergent(_) => f.write_str("Divergent"), } } @@ -8411,6 +8423,7 @@ impl<'db> TypeVarInstance<'db> { DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression + | DynamicType::TodoFunctionalTypedDict | DynamicType::TodoTypeVarTuple => Parameters::todo(), DynamicType::Any | DynamicType::Unknown diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index b3a7b4b73b720..ae7d999f4d8a8 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -49,7 +49,6 @@ use crate::types::{ KnownClass, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, TypeVarVariance, UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, - todo_type, }; use crate::unpack::EvaluationMode; use crate::{DisplaySettings, Program}; @@ -1956,7 +1955,9 @@ impl<'db> Bindings<'db> { }, Type::SpecialForm(SpecialFormType::TypedDict) => { - overload.set_return_type(todo_type!("Support for functional `TypedDict`")); + overload.set_return_type(Type::Dynamic( + crate::types::DynamicType::TodoFunctionalTypedDict, + )); } // Not a special case diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 520846c731c1f..bfc189535ac18 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -59,6 +59,7 @@ impl<'db> ClassBase<'db> { ClassBase::Dynamic(DynamicType::UnspecializedTypeVar) => "UnspecializedTypeVar", ClassBase::Dynamic( DynamicType::Todo(_) + | DynamicType::TodoFunctionalTypedDict | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression | DynamicType::TodoTypeVarTuple, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 1d89d403771a3..3c512c5140e1a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -117,7 +117,7 @@ use crate::types::generics::{ GenericContext, InferableTypeVars, SpecializationBuilder, bind_typevar, enclosing_generic_contexts, typing_self, }; -use crate::types::infer::nearest_enclosing_function; +use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function}; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; use crate::types::special_form::AliasSpec; @@ -9232,6 +9232,41 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } + + // `Required`, `NotRequired`, and `ReadOnly` are only valid inside TypedDict classes. + if declared.qualifiers.intersects( + TypeQualifiers::REQUIRED | TypeQualifiers::NOT_REQUIRED | TypeQualifiers::READ_ONLY, + ) { + let in_typed_dict = current_scope.kind() == ScopeKind::Class + && nearest_enclosing_class(self.db(), self.index, self.scope()).is_some_and( + |class| { + class.iter_mro(self.db(), None).any(|base| { + matches!( + base, + ClassBase::TypedDict + | ClassBase::Dynamic(DynamicType::TodoFunctionalTypedDict) + ) + }) + }, + ); + if !in_typed_dict { + for qualifier in [ + TypeQualifiers::REQUIRED, + TypeQualifiers::NOT_REQUIRED, + TypeQualifiers::READ_ONLY, + ] { + if declared.qualifiers.contains(qualifier) + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, annotation) + { + builder.into_diagnostic(format_args!( + "`{name}` is only allowed in TypedDict fields", + name = qualifier.name() + )); + } + } + } + } } if target @@ -11615,7 +11650,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Avoid false positives for the functional `TypedDict` form, which is currently // unsupported. - if let Some(Type::Dynamic(DynamicType::Todo(_))) = tcx.annotation { + if let Some(Type::Dynamic(DynamicType::TodoFunctionalTypedDict)) = tcx.annotation { return KnownClass::Dict .to_specialized_instance(self.db(), &[Type::unknown(), Type::unknown()]); } @@ -14325,6 +14360,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _, _) | (_, typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _) => Some(typevar), + (todo @ Type::Dynamic(DynamicType::TodoFunctionalTypedDict), _, _) + | (_, todo @ Type::Dynamic(DynamicType::TodoFunctionalTypedDict), _) => Some(todo), + // When both operands are the same constrained TypeVar (e.g., `T: (int, str)`), // we check if the operation is valid for each constraint paired with itself. // This is different from treating it as a union, where we'd check all combinations. diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 7a245bd545f33..9d3a807a36b5b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -309,6 +309,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "`ClassVar` cannot contain type variables", ); } + + // Reject nested `Required`/`NotRequired`, e.g. + // `Required[Required[int]]` or `Required[NotRequired[int]]`. + if matches!( + qualifier, + TypeQualifier::Required | TypeQualifier::NotRequired + ) && type_and_qualifiers.qualifiers.intersects( + TypeQualifiers::REQUIRED | TypeQualifiers::NOT_REQUIRED, + ) && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "`{qualifier}` cannot be nested inside \ + `Required` or `NotRequired`", + )); + } + type_and_qualifiers.with_qualifier(TypeQualifiers::from(qualifier)) } else { for element in arguments { diff --git a/crates/ty_test/src/matcher.rs b/crates/ty_test/src/matcher.rs index cabbd32305deb..415ac2bf7419d 100644 --- a/crates/ty_test/src/matcher.rs +++ b/crates/ty_test/src/matcher.rs @@ -212,6 +212,7 @@ fn discard_todo_metadata(ty: &str) -> Cow<'_, str> { "@Todo(StarredExpression)", "@Todo(typing.Unpack)", "@Todo(TypeVarTuple)", + "@Todo(Functional TypedDicts)", ]; static TODO_METADATA_REGEX: LazyLock = From 7015ea7481261274e9782adeb6fabf65cff23918 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 28 Feb 2026 14:27:48 +0000 Subject: [PATCH 135/261] [ty] Validate type variable defaults don't reference later type parameters or type parameters out of scope (#23623) --- .../mdtest/generics/legacy/classes.md | 37 +++ .../mdtest/generics/legacy/paramspec.md | 6 +- .../mdtest/generics/legacy/variables.md | 2 +- .../mdtest/generics/pep695/classes.md | 15 ++ .../mdtest/generics/pep695/paramspec.md | 2 +- ...neric\342\200\246_(5a066394f338af48).snap" | 249 ++++++++++++++++++ ...o_back-references_(9051beb16a623d36).snap" | 111 ++++++++ .../src/types/diagnostic.rs | 47 ++++ .../src/types/infer/builder.rs | 43 +++ .../ty_python_semantic/src/types/visitor.rs | 85 ++++-- 10 files changed, 572 insertions(+), 25 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 8c9774d18b070..a94d5cb464899 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -137,6 +137,8 @@ reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTyp ## Specializing generic classes explicitly + + The type parameter can be specified explicitly: ```py @@ -219,6 +221,41 @@ reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] ``` +Type variable defaults can reference earlier type variables, but not later ones: + +```py +from typing_extensions import TypeVar, Generic + +WithDefaultT1 = TypeVar("WithDefaultT1", default=int) +WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) + +# This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it +class GoodOrder(Generic[WithDefaultT1, WithDefaultT2]): ... + +# error: [invalid-generic-class] "Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1`" +class BadOrder(Generic[WithDefaultT2, WithDefaultT1]): ... + +WithDefaultU = TypeVar("WithDefaultU", default=int) + +# error: [invalid-generic-class] +class AlsoBadOrder(Generic[WithDefaultT2, WithDefaultT1, WithDefaultU]): ... +``` + +A type variable default cannot reference a type variable that is not a type parameter of the class: + +```py +from typing_extensions import TypeVar, Generic + +StartT = TypeVar("StartT", default=int) +StopT = TypeVar("StopT", default=StartT) +StepT = TypeVar("StepT", default=int | None) +Start2T = TypeVar("Start2T", default="StopT") +Stop2T = TypeVar("Stop2T", default=int) + +# error: [invalid-generic-class] "Default of `Start2T` cannot reference out-of-scope type variable `StopT`" +class Bad(Generic[Start2T, Stop2T, StepT]): ... +``` + ## Diagnostics for bad specializations We show the user where the type variable was defined if a specialization is given that doesn't diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index ef0c17e4e9d28..63465627c0bdf 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -425,11 +425,13 @@ reveal_type(p3.attr1) # revealed: (int, /) -> None reveal_type(p3.attr2) # revealed: (str, /) -> None # Un-ordered type variables as the default of `PAnother` is `P` -class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-generic-class] +# error: [invalid-generic-class] +# error: [invalid-generic-class] +class ParamSpecWithDefault5(Generic[PAnother, P]): attr: Callable[PAnother, None] -# TODO: error # PAnother has default as P (another ParamSpec) which is not in scope +# error: [invalid-generic-class] class ParamSpecWithDefault6(Generic[PAnother]): attr: Callable[PAnother, None] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 55bbd50076a08..c0203eadc96ce 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -176,7 +176,7 @@ reveal_type(Valid[int]()) # revealed: Valid[int, int, int] reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str] reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None] -# TODO: error, default value for U isn't available in the generic context +# error: [invalid-generic-class] "Default of `U` cannot reference out-of-scope type variable `T`" class Invalid(Generic[U]): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 1d26718721197..c09f91e804dd1 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -763,6 +763,8 @@ def protocol_case(x: GenericProtocol[[int], str]) -> None: ### No back-references + + Typevar bounds/constraints/defaults are lazy, but cannot refer to later typevars. Furthermore, bounds/constraints cannot refer to other type variables, i.e. they must be non-generic. @@ -785,6 +787,19 @@ class F[S: X]: X = int ``` +Type variable defaults can reference earlier type variables, but not later ones: + +```py +# This is fine: U's default references T, which comes before U +class Good[T, U = T]: ... + +# error: [invalid-generic-class] "Default of `S` cannot reference later type parameter `T`" +class Bad[S = T, T = int]: ... + +# error: [invalid-generic-class] +class AlsoBad[S = list[T], T = int]: ... +``` + ## Cyclic class definitions ### F-bounded quantification diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index fc818abfdca3d..2c75fa153f64d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -370,7 +370,7 @@ reveal_type(p3.attr2) # revealed: (str, /) -> None P2 = ParamSpec("P2") -# TODO: error: paramspec is out of scope +# error: [invalid-generic-class] "Default of `P1` cannot reference out-of-scope type variable `P2`" class ParamSpecWithDefault5[**P1 = P2]: attr: Callable[P1, None] ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" new file mode 100644 index 0000000000000..e92d1159c34d2 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" @@ -0,0 +1,249 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: classes.md - Generic classes: Legacy syntax - Specializing generic classes explicitly +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import Generic, Literal, TypeVar + 2 | + 3 | T = TypeVar("T") + 4 | + 5 | class C(Generic[T]): + 6 | x: T + 7 | + 8 | reveal_type(C[int]()) # revealed: C[int] + 9 | reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]] +10 | # error: [invalid-type-arguments] "Too many type arguments to class `C`: expected 1, got 2" +11 | reveal_type(C[int, int]()) # revealed: C[Unknown] +12 | from typing import Union +13 | +14 | BoundedT = TypeVar("BoundedT", bound=int) +15 | BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str]) +16 | +17 | class Bounded(Generic[BoundedT]): ... +18 | class BoundedByUnion(Generic[BoundedByUnionT]): ... +19 | class IntSubclass(int): ... +20 | +21 | reveal_type(Bounded[int]()) # revealed: Bounded[int] +22 | reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] +23 | +24 | # error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`" +25 | reveal_type(Bounded[str]()) # revealed: Bounded[Unknown] +26 | +27 | # error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`" +28 | reveal_type(Bounded[int | str]()) # revealed: Bounded[Unknown] +29 | +30 | reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int] +31 | reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass] +32 | reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str] +33 | reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str] +34 | ConstrainedT = TypeVar("ConstrainedT", int, str) +35 | +36 | class Constrained(Generic[ConstrainedT]): ... +37 | +38 | reveal_type(Constrained[int]()) # revealed: Constrained[int] +39 | +40 | # TODO: error: [invalid-argument-type] +41 | # TODO: revealed: Constrained[Unknown] +42 | reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass] +43 | +44 | reveal_type(Constrained[str]()) # revealed: Constrained[str] +45 | +46 | # TODO: error: [invalid-argument-type] +47 | # TODO: revealed: Unknown +48 | reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] +49 | +50 | # error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `ConstrainedT@Constrained`" +51 | reveal_type(Constrained[object]()) # revealed: Constrained[Unknown] +52 | WithDefaultU = TypeVar("WithDefaultU", default=int) +53 | +54 | class WithDefault(Generic[T, WithDefaultU]): ... +55 | +56 | reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] +57 | reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] +58 | from typing_extensions import TypeVar, Generic +59 | +60 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) +61 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) +62 | +63 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it +64 | class GoodOrder(Generic[WithDefaultT1, WithDefaultT2]): ... +65 | +66 | # error: [invalid-generic-class] "Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1`" +67 | class BadOrder(Generic[WithDefaultT2, WithDefaultT1]): ... +68 | +69 | WithDefaultU = TypeVar("WithDefaultU", default=int) +70 | +71 | # error: [invalid-generic-class] +72 | class AlsoBadOrder(Generic[WithDefaultT2, WithDefaultT1, WithDefaultU]): ... +73 | from typing_extensions import TypeVar, Generic +74 | +75 | StartT = TypeVar("StartT", default=int) +76 | StopT = TypeVar("StopT", default=StartT) +77 | StepT = TypeVar("StepT", default=int | None) +78 | Start2T = TypeVar("Start2T", default="StopT") +79 | Stop2T = TypeVar("Stop2T", default=int) +80 | +81 | # error: [invalid-generic-class] "Default of `Start2T` cannot reference out-of-scope type variable `StopT`" +82 | class Bad(Generic[Start2T, Stop2T, StepT]): ... +``` + +# Diagnostics + +``` +error[invalid-type-arguments]: Too many type arguments to class `C`: expected 1, got 2 + --> src/mdtest_snippet.py:11:20 + | + 9 | reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]] +10 | # error: [invalid-type-arguments] "Too many type arguments to class `C`: expected 1, got 2" +11 | reveal_type(C[int, int]()) # revealed: C[Unknown] + | ^^^ +12 | from typing import Union + | +info: rule `invalid-type-arguments` is enabled by default + +``` + +``` +error[invalid-type-arguments]: Type `str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded` + --> src/mdtest_snippet.py:25:21 + | +24 | # error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`" +25 | reveal_type(Bounded[str]()) # revealed: Bounded[Unknown] + | ^^^ +26 | +27 | # error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`" + | + ::: src/mdtest_snippet.py:14:1 + | +12 | from typing import Union +13 | +14 | BoundedT = TypeVar("BoundedT", bound=int) + | -------- Type variable defined here +15 | BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str]) + | +info: rule `invalid-type-arguments` is enabled by default + +``` + +``` +error[invalid-type-arguments]: Type `int | str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded` + --> src/mdtest_snippet.py:28:21 + | +27 | # error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`" +28 | reveal_type(Bounded[int | str]()) # revealed: Bounded[Unknown] + | ^^^^^^^^^ +29 | +30 | reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int] + | + ::: src/mdtest_snippet.py:14:1 + | +12 | from typing import Union +13 | +14 | BoundedT = TypeVar("BoundedT", bound=int) + | -------- Type variable defined here +15 | BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str]) + | +info: rule `invalid-type-arguments` is enabled by default + +``` + +``` +error[invalid-type-arguments]: Type `object` does not satisfy constraints `int`, `str` of type variable `ConstrainedT@Constrained` + --> src/mdtest_snippet.py:51:25 + | +50 | # error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `ConstrainedT@Constrained`" +51 | reveal_type(Constrained[object]()) # revealed: Constrained[Unknown] + | ^^^^^^ +52 | WithDefaultU = TypeVar("WithDefaultU", default=int) + | + ::: src/mdtest_snippet.py:34:1 + | +32 | reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str] +33 | reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str] +34 | ConstrainedT = TypeVar("ConstrainedT", int, str) + | ------------ Type variable defined here +35 | +36 | class Constrained(Generic[ConstrainedT]): ... + | +info: rule `invalid-type-arguments` is enabled by default + +``` + +``` +error[invalid-generic-class]: Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1` + --> src/mdtest_snippet.py:67:7 + | +66 | # error: [invalid-generic-class] "Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1`" +67 | class BadOrder(Generic[WithDefaultT2, WithDefaultT1]): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +68 | +69 | WithDefaultU = TypeVar("WithDefaultU", default=int) + | + ::: src/mdtest_snippet.py:60:1 + | +58 | from typing_extensions import TypeVar, Generic +59 | +60 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) + | ----------------------------------------------------- `WithDefaultT1` defined here +61 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) + | --------------------------------------------------------------- `WithDefaultT2` defined here +62 | +63 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1` + --> src/mdtest_snippet.py:72:7 + | +71 | # error: [invalid-generic-class] +72 | class AlsoBadOrder(Generic[WithDefaultT2, WithDefaultT1, WithDefaultU]): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +73 | from typing_extensions import TypeVar, Generic + | + ::: src/mdtest_snippet.py:60:1 + | +58 | from typing_extensions import TypeVar, Generic +59 | +60 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) + | ----------------------------------------------------- `WithDefaultT1` defined here +61 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) + | --------------------------------------------------------------- `WithDefaultT2` defined here +62 | +63 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Default of `Start2T` cannot reference out-of-scope type variable `StopT` + --> src/mdtest_snippet.py:82:7 + | +81 | # error: [invalid-generic-class] "Default of `Start2T` cannot reference out-of-scope type variable `StopT`" +82 | class Bad(Generic[Start2T, Stop2T, StepT]): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + ::: src/mdtest_snippet.py:78:1 + | +76 | StopT = TypeVar("StopT", default=StartT) +77 | StepT = TypeVar("StepT", default=int | None) +78 | Start2T = TypeVar("Start2T", default="StopT") + | --------------------------------------------- `Start2T` defined here +79 | Stop2T = TypeVar("Stop2T", default=int) + | +info: rule `invalid-generic-class` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" new file mode 100644 index 0000000000000..9ba78f1eebbd6 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" @@ -0,0 +1,111 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: classes.md - Generic classes: PEP 695 syntax - Scoping of typevars - No back-references +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | # error: [invalid-type-variable-bound] + 2 | class C[S: T, T]: + 3 | pass + 4 | + 5 | # error: [invalid-type-variable-bound] + 6 | class D[S, T: S]: + 7 | pass + 8 | + 9 | # error: [invalid-type-variable-constraints] +10 | class E[S: (int, T), T]: +11 | pass +12 | +13 | class F[S: X]: +14 | pass +15 | +16 | X = int +17 | # This is fine: U's default references T, which comes before U +18 | class Good[T, U = T]: ... +19 | +20 | # error: [invalid-generic-class] "Default of `S` cannot reference later type parameter `T`" +21 | class Bad[S = T, T = int]: ... +22 | +23 | # error: [invalid-generic-class] +24 | class AlsoBad[S = list[T], T = int]: ... +``` + +# Diagnostics + +``` +error[invalid-type-variable-bound]: TypeVar upper bound cannot be generic + --> src/mdtest_snippet.py:2:12 + | +1 | # error: [invalid-type-variable-bound] +2 | class C[S: T, T]: + | ^ +3 | pass + | +info: rule `invalid-type-variable-bound` is enabled by default + +``` + +``` +error[invalid-type-variable-bound]: TypeVar upper bound cannot be generic + --> src/mdtest_snippet.py:6:15 + | +5 | # error: [invalid-type-variable-bound] +6 | class D[S, T: S]: + | ^ +7 | pass + | +info: rule `invalid-type-variable-bound` is enabled by default + +``` + +``` +error[invalid-type-variable-constraints]: TypeVar constraint cannot be generic + --> src/mdtest_snippet.py:10:18 + | + 9 | # error: [invalid-type-variable-constraints] +10 | class E[S: (int, T), T]: + | ^ +11 | pass + | +info: rule `invalid-type-variable-constraints` is enabled by default + +``` + +``` +error[invalid-generic-class]: Default of `S` cannot reference later type parameter `T` + --> src/mdtest_snippet.py:21:7 + | +20 | # error: [invalid-generic-class] "Default of `S` cannot reference later type parameter `T`" +21 | class Bad[S = T, T = int]: ... + | ^^^ ----- ------- `T` defined here + | | + | `S` defined here +22 | +23 | # error: [invalid-generic-class] + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Default of `S` cannot reference later type parameter `T` + --> src/mdtest_snippet.py:24:7 + | +23 | # error: [invalid-generic-class] +24 | class AlsoBad[S = list[T], T = int]: ... + | ^^^^^^^ ----------- ------- `T` defined here + | | + | `S` defined here + | +info: rule `invalid-generic-class` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 3256a7a2cf1c3..debd3775a7c91 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -4942,6 +4942,53 @@ pub(crate) fn report_invalid_type_param_order<'db>( } } +pub(crate) fn report_invalid_typevar_default_reference<'db>( + context: &InferContext<'db, '_>, + class: StaticClassLiteral<'db>, + typevar_with_bad_default: TypeVarInstance<'db>, + referenced_typevar: TypeVarInstance<'db>, + is_later_in_list: bool, +) { + let db = context.db(); + + let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, class.header_range(db)) else { + return; + }; + + let mut diagnostic = if is_later_in_list { + builder.into_diagnostic(format_args!( + "Default of `{}` cannot reference later type parameter `{}`", + typevar_with_bad_default.name(db), + referenced_typevar.name(db), + )) + } else { + builder.into_diagnostic(format_args!( + "Default of `{}` cannot reference out-of-scope type variable `{}`", + typevar_with_bad_default.name(db), + referenced_typevar.name(db), + )) + }; + + let typevars_to_annotate = if is_later_in_list { + &[typevar_with_bad_default, referenced_typevar][..] + } else { + &[typevar_with_bad_default][..] + }; + + for tvar in typevars_to_annotate { + let Some(definition) = tvar.definition(db) else { + continue; + }; + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{}` defined here", tvar.name(db))), + ); + } +} + pub(crate) fn report_shadowed_type_variable<'db>( context: &InferContext<'db, '_>, typevar_name: &ast::name::Name, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3c512c5140e1a..66bc5c025b545 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -101,6 +101,7 @@ use crate::types::diagnostic::{ report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_total_ordering, report_invalid_type_checking_constant, report_invalid_type_param_order, + report_invalid_typevar_default_reference, report_match_pattern_against_non_runtime_checkable_protocol, report_match_pattern_against_typed_dict, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable, @@ -128,6 +129,7 @@ use crate::types::typed_dict::{ TypedDictAssignmentKind, TypedDictKeyAssignment, validate_typed_dict_constructor, validate_typed_dict_dict_literal, }; +use crate::types::visitor::find_over_type; use crate::types::{ BoundTypeVarIdentity, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypeKind, ClassType, DataclassParams, DynamicType, InternedConstraintSet, InternedType, @@ -1446,6 +1448,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + // Check that type variable defaults only reference type variables + // that precede them in the type parameter list. + if let Some(generic_context) = class + .pep695_generic_context(self.db()) + .or(class.legacy_generic_context(self.db())) + { + let db = self.db(); + let typevars = generic_context.variables(db).map(|btv| btv.typevar(db)); + + // `variables` should be fairly cheap to clone; it's just several cheap wrappers around + // a `std::slice::Iter` under the hood. + for (i, typevar) in typevars.clone().enumerate() { + let Some(default_ty) = typevar.default_type(db) else { + continue; + }; + + let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let tvar = match t { + Type::TypeVar(tvar) => tvar.typevar(db), + Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, + _ => return None, + }; + if !typevars.clone().take(i).contains(&tvar) { + Some(tvar) + } else { + None + } + }); + if let Some(bad_typevar) = first_bad_tvar { + let is_later_in_list = typevars.clone().skip(i).contains(&bad_typevar); + report_invalid_typevar_default_reference( + &self.context, + class, + typevar, + bad_typevar, + is_later_in_list, + ); + } + } + } + let scope = class.body_scope(self.db()).scope(self.db()); if let Some(parent) = scope.parent() { // Check that the class's own type parameters don't shadow diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index b19e95e51372a..cccfa2ed6e967 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -282,40 +282,41 @@ impl<'db> TypeCollector<'db> { } } -/// Return `true` if `ty`, or any of the types contained in `ty`, match the closure passed in. -/// -/// The function guards against infinite recursion -/// by keeping track of the non-atomic types it has already seen. -/// -/// The `should_visit_lazy_type_attributes` parameter controls whether deferred type attributes -/// (value of a type alias, attributes of a class-based protocol, bounds/constraints of a typevar) -/// are visited or not. -pub(super) fn any_over_type<'db>( +/// Implementation for `any_over_type` and `find_over_type`. +fn any_over_type_impl<'db, F, T>( db: &'db dyn Db, ty: Type<'db>, should_visit_lazy_type_attributes: bool, - query: impl Fn(Type<'db>) -> bool, -) -> bool { - struct AnyOverTypeVisitor<'db, 'a> { - query: &'a dyn Fn(Type<'db>) -> bool, + query: F, +) -> T +where + T: Copy + Default + PartialEq, + F: Fn(Type<'db>) -> T, +{ + struct AnyOverTypeVisitor<'db, 'a, U> { + query: &'a dyn Fn(Type<'db>) -> U, recursion_guard: TypeCollector<'db>, - found_matching_type: Cell, + found_matching_type: Cell, should_visit_lazy_type_attributes: bool, } - impl<'db> TypeVisitor<'db> for AnyOverTypeVisitor<'db, '_> { + impl<'db, U> TypeVisitor<'db> for AnyOverTypeVisitor<'db, '_, U> + where + U: Copy + Default + PartialEq, + { fn should_visit_lazy_type_attributes(&self) -> bool { self.should_visit_lazy_type_attributes } fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { - let already_found = self.found_matching_type.get(); - if already_found { + let default_value = U::default(); + let pre_existing = self.found_matching_type.get(); + if pre_existing != default_value { return; } - let found = already_found | (self.query)(ty); - self.found_matching_type.set(found); - if found { + let new_value = (self.query)(ty); + self.found_matching_type.set(new_value); + if new_value != default_value { return; } walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); @@ -325,9 +326,51 @@ pub(super) fn any_over_type<'db>( let visitor = AnyOverTypeVisitor { query: &query, recursion_guard: TypeCollector::default(), - found_matching_type: Cell::new(false), + found_matching_type: Cell::default(), should_visit_lazy_type_attributes, }; visitor.visit_type(db, ty); visitor.found_matching_type.get() } + +/// Return `true` if `ty`, or any of the types contained in `ty`, match the closure passed in. +/// +/// The function guards against infinite recursion +/// by keeping track of the non-atomic types it has already seen. +/// +/// The `should_visit_lazy_type_attributes` parameter controls whether deferred type attributes +/// (value of a type alias, attributes of a class-based protocol, bounds/constraints of a typevar) +/// are visited or not. +pub(super) fn any_over_type<'db>( + db: &'db dyn Db, + ty: Type<'db>, + should_visit_lazy_type_attributes: bool, + query: impl Fn(Type<'db>) -> bool, +) -> bool { + any_over_type_impl(db, ty, should_visit_lazy_type_attributes, query) +} + +/// Recurse into a type and calls the passed-in closure on every nested type +/// encountered, returning the first non-`None` value returned by the closure. +/// +/// For example, if `ty` is `list[tuple[int, T]]` where `T` is a type variable +/// and the closure passed in is `|t| matches!(t, Type::TypeVar(_))`, then this +/// function will return `Some(T)`. +/// +/// The function guards against infinite recursion +/// by keeping track of the non-atomic types it has already seen. +/// +/// The `should_visit_lazy_type_attributes` parameter controls whether deferred type attributes +/// (value of a type alias, attributes of a class-based protocol, bounds/constraints of a typevar) +/// are visited or not. +pub(super) fn find_over_type<'db, T>( + db: &'db dyn Db, + ty: Type<'db>, + should_visit_lazy_type_attributes: bool, + query: impl Fn(Type<'db>) -> Option, +) -> Option +where + T: Copy + PartialEq, +{ + any_over_type_impl(db, ty, should_visit_lazy_type_attributes, query) +} From 63e0d0d80f4e080e9715562124a0b6bb0eb5f6c3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 28 Feb 2026 14:35:48 +0000 Subject: [PATCH 136/261] [ty] Detect inconsistent generic base class specializations (#23615) --- .../mdtest/generics/legacy/classes.md | 63 +++++ .../mdtest/instance_layout_conflict.md | 9 +- ...nsist\342\200\246_(557742f3cd2464b2).snap" | 221 ++++++++++++++++++ ...mplic\342\200\246_(4c3d127986a58f11).snap" | 43 ++-- ...s_hie\342\200\246_(5e8fca10d966c36e).snap" | 1 - .../resources/mdtest/type_compendium/tuple.md | 15 +- .../src/types/infer/builder.rs | 131 ++++++++++- 7 files changed, 455 insertions(+), 28 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" rename "crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Built-ins_with_impli\342\200\246_(f5857d64ce69ca1d).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" (77%) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index a94d5cb464899..6601893b3929c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -135,6 +135,69 @@ reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) ``` +## Errors for inconsistent type arguments + + + +When inheriting from the same generic ancestor through multiple paths, the type argument ordering +must be consistent. It is an error for a class to explicitly inherit from a generic base class that +also appears elsewhere in the MRO with a different specialization: + +```py +from typing import TypeVar, Generic, Any + +T1 = TypeVar("T1") +T2 = TypeVar("T2") + +class Grandparent(Generic[T1, T2]): ... +class Parent(Grandparent[T1, T2]): ... + +# Consistent ordering is fine: +class GoodChild(Parent[T1, T2], Grandparent[T1, T2]): ... + +# error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild, T1@BadChild]` and `Grandparent[T1@BadChild, T2@BadChild]`" +class BadChild(Parent[T1, T2], Grandparent[T2, T1]): ... + +# The same applies when the explicit base is partially specialized differently: +class Parent2(Grandparent[T1, T2]): ... + +# error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild2, int]` and `Grandparent[T1@BadChild2, T2@BadChild2]`" +class BadChild2(Parent2[T1, T2], Grandparent[T2, int]): ... + +# The inconsistency can also come through two intermediate classes (diamond): +class Parent3(Grandparent[T1, T2]): ... +class Parent4(Grandparent[T1, T2]): ... + +# error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild3, T1@BadChild3]` and `Grandparent[T1@BadChild3, T2@BadChild3]`" +class BadChild3(Parent3[T1, T2], Parent4[T2, T1]): ... + +# Implicit specialization is fine: +class Fine(Parent, Grandparent[T1, T2]): ... +class AlsoFine(Parent3, Parent4[T1, T2]): ... +class Dandy(Parent, Parent3, Parent4): ... + +# Edge cases: the first class is implicitly specialized +# (or explicitly specialized with `Any`s), but later classes are not: + +# error: [invalid-generic-class] +class BadChild4(Parent, Parent3[T1, T2], Parent4[T2, T1]): ... + +# error: [invalid-generic-class] +class BadChild5(Parent[Any, Any], Parent3[T1, T2], Parent4[T2, T1]): ... + +# error: [invalid-generic-class] +class BadChild6(Parent[T1, T2], Parent3, Parent4[T2, T1]): ... + +# error: [invalid-generic-class] +class BadChild7(Parent[T1, T2], Parent3[Any, Any], Parent4[T2, T1]): ... + +# error: [invalid-generic-class] +class BadChild8(Parent[T1, T2], Parent3[T2, T1], Parent4): ... + +# error: [invalid-generic-class] +class BadChild9(Parent[T1, T2], Parent3[T2, T1], Parent4[Any, Any]): ... +``` + ## Specializing generic classes explicitly diff --git a/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md index bb8e013083adf..30d6624a3807f 100644 --- a/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md +++ b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md @@ -171,7 +171,7 @@ class Bar: class Baz(Foo, Bar): ... # fine ``` -## Built-ins with implicit layouts +## Builtins with implicit layouts @@ -206,7 +206,7 @@ class E( # error: [instance-layout-conflict] str ): ... -class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] +class F(int, bytes, bytearray): ... # error: [instance-layout-conflict] @disjoint_base class G: ... @@ -223,9 +223,12 @@ class I( # error: [instance-layout-conflict] ``` We avoid emitting an `instance-layout-conflict` diagnostic for this class definition, because -`range` is `@final`, so we'll complain about the `class` statement anyway: +`range` is `@final`, so we'll complain about the `class` statement anyway. (We also emit +`invalid-generic-class` here, as `Sequence[str]` and `Sequence[int]` coexist invalidly in this +class's MRO.) ```py +# error: [invalid-generic-class] class Foo(range, str): ... # error: [subclass-of-final-class] ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" new file mode 100644 index 0000000000000..2e96af31f3f1c --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" @@ -0,0 +1,221 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: classes.md - Generic classes: Legacy syntax - Errors for inconsistent type arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar, Generic, Any + 2 | + 3 | T1 = TypeVar("T1") + 4 | T2 = TypeVar("T2") + 5 | + 6 | class Grandparent(Generic[T1, T2]): ... + 7 | class Parent(Grandparent[T1, T2]): ... + 8 | + 9 | # Consistent ordering is fine: +10 | class GoodChild(Parent[T1, T2], Grandparent[T1, T2]): ... +11 | +12 | # error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild, T1@BadChild]` and `Grandparent[T1@BadChild, T2@BadChild]`" +13 | class BadChild(Parent[T1, T2], Grandparent[T2, T1]): ... +14 | +15 | # The same applies when the explicit base is partially specialized differently: +16 | class Parent2(Grandparent[T1, T2]): ... +17 | +18 | # error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild2, int]` and `Grandparent[T1@BadChild2, T2@BadChild2]`" +19 | class BadChild2(Parent2[T1, T2], Grandparent[T2, int]): ... +20 | +21 | # The inconsistency can also come through two intermediate classes (diamond): +22 | class Parent3(Grandparent[T1, T2]): ... +23 | class Parent4(Grandparent[T1, T2]): ... +24 | +25 | # error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild3, T1@BadChild3]` and `Grandparent[T1@BadChild3, T2@BadChild3]`" +26 | class BadChild3(Parent3[T1, T2], Parent4[T2, T1]): ... +27 | +28 | # Implicit specialization is fine: +29 | class Fine(Parent, Grandparent[T1, T2]): ... +30 | class AlsoFine(Parent3, Parent4[T1, T2]): ... +31 | class Dandy(Parent, Parent3, Parent4): ... +32 | +33 | # Edge cases: the first class is implicitly specialized +34 | # (or explicitly specialized with `Any`s), but later classes are not: +35 | +36 | # error: [invalid-generic-class] +37 | class BadChild4(Parent, Parent3[T1, T2], Parent4[T2, T1]): ... +38 | +39 | # error: [invalid-generic-class] +40 | class BadChild5(Parent[Any, Any], Parent3[T1, T2], Parent4[T2, T1]): ... +41 | +42 | # error: [invalid-generic-class] +43 | class BadChild6(Parent[T1, T2], Parent3, Parent4[T2, T1]): ... +44 | +45 | # error: [invalid-generic-class] +46 | class BadChild7(Parent[T1, T2], Parent3[Any, Any], Parent4[T2, T1]): ... +47 | +48 | # error: [invalid-generic-class] +49 | class BadChild8(Parent[T1, T2], Parent3[T2, T1], Parent4): ... +50 | +51 | # error: [invalid-generic-class] +52 | class BadChild9(Parent[T1, T2], Parent3[T2, T1], Parent4[Any, Any]): ... +``` + +# Diagnostics + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:13:7 + | +12 | # error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild, T1@BadChild]` … +13 | class BadChild(Parent[T1, T2], Grandparent[T2, T1]): ... + | ^^^^^^^^^--------------^^-------------------^ + | | | + | | Later class base is `Grandparent[T2@BadChild, T1@BadChild]` + | Earlier class base inherits from `Grandparent[T1@BadChild, T2@BadChild]` +14 | +15 | # The same applies when the explicit base is partially specialized differently: + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:19:7 + | +18 | # error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild2, int]` and `Gr… +19 | class BadChild2(Parent2[T1, T2], Grandparent[T2, int]): ... + | ^^^^^^^^^^---------------^^--------------------^ + | | | + | | Later class base is `Grandparent[T2@BadChild2, int]` + | Earlier class base inherits from `Grandparent[T1@BadChild2, T2@BadChild2]` +20 | +21 | # The inconsistency can also come through two intermediate classes (diamond): + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:26:7 + | +25 | # error: [invalid-generic-class] "Inconsistent type arguments: class cannot inherit from both `Grandparent[T2@BadChild3, T1@BadChild3]… +26 | class BadChild3(Parent3[T1, T2], Parent4[T2, T1]): ... + | ^^^^^^^^^^---------------^^---------------^ + | | | + | | Later class base inherits from `Grandparent[T2@BadChild3, T1@BadChild3]` + | Earlier class base inherits from `Grandparent[T1@BadChild3, T2@BadChild3]` +27 | +28 | # Implicit specialization is fine: + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:37:7 + | +36 | # error: [invalid-generic-class] +37 | class BadChild4(Parent, Parent3[T1, T2], Parent4[T2, T1]): ... + | ^^^^^^^^^^^^^^^^^^---------------^^---------------^ + | | | + | | Later class base inherits from `Grandparent[T2@BadChild4, T1@BadChild4]` + | Earlier class base inherits from `Grandparent[T1@BadChild4, T2@BadChild4]` +38 | +39 | # error: [invalid-generic-class] + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:40:7 + | +39 | # error: [invalid-generic-class] +40 | class BadChild5(Parent[Any, Any], Parent3[T1, T2], Parent4[T2, T1]): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^---------------^^---------------^ + | | | + | | Later class base inherits from `Grandparent[T2@BadChild5, T1@BadChild5]` + | Earlier class base inherits from `Grandparent[T1@BadChild5, T2@BadChild5]` +41 | +42 | # error: [invalid-generic-class] + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:43:7 + | +42 | # error: [invalid-generic-class] +43 | class BadChild6(Parent[T1, T2], Parent3, Parent4[T2, T1]): ... + | ^^^^^^^^^^--------------^^^^^^^^^^^---------------^ + | | | + | | Later class base inherits from `Grandparent[T2@BadChild6, T1@BadChild6]` + | Earlier class base inherits from `Grandparent[T1@BadChild6, T2@BadChild6]` +44 | +45 | # error: [invalid-generic-class] + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:46:7 + | +45 | # error: [invalid-generic-class] +46 | class BadChild7(Parent[T1, T2], Parent3[Any, Any], Parent4[T2, T1]): ... + | ^^^^^^^^^^--------------^^^^^^^^^^^^^^^^^^^^^---------------^ + | | | + | | Later class base inherits from `Grandparent[T2@BadChild7, T1@BadChild7]` + | Earlier class base inherits from `Grandparent[T1@BadChild7, T2@BadChild7]` +47 | +48 | # error: [invalid-generic-class] + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:49:7 + | +48 | # error: [invalid-generic-class] +49 | class BadChild8(Parent[T1, T2], Parent3[T2, T1], Parent4): ... + | ^^^^^^^^^^--------------^^---------------^^^^^^^^^^ + | | | + | | Later class base inherits from `Grandparent[T2@BadChild8, T1@BadChild8]` + | Earlier class base inherits from `Grandparent[T1@BadChild8, T2@BadChild8]` +50 | +51 | # error: [invalid-generic-class] + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` among class bases + --> src/mdtest_snippet.py:52:7 + | +51 | # error: [invalid-generic-class] +52 | class BadChild9(Parent[T1, T2], Parent3[T2, T1], Parent4[Any, Any]): ... + | ^^^^^^^^^^--------------^^---------------^^^^^^^^^^^^^^^^^^^^ + | | | + | | Later class base inherits from `Grandparent[T2@BadChild9, T1@BadChild9]` + | Earlier class base inherits from `Grandparent[T1@BadChild9, T2@BadChild9]` + | +info: rule `invalid-generic-class` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Built-ins_with_impli\342\200\246_(f5857d64ce69ca1d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" similarity index 77% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Built-ins_with_impli\342\200\246_(f5857d64ce69ca1d).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" index c391af908613c..f77d93ad5a75b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Built-ins_with_impli\342\200\246_(f5857d64ce69ca1d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" @@ -4,7 +4,7 @@ expression: snapshot --- --- -mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - Built-ins with implicit layouts +mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - Builtins with implicit layouts mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md --- @@ -36,7 +36,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict 21 | str 22 | ): ... 23 | -24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] +24 | class F(int, bytes, bytearray): ... # error: [instance-layout-conflict] 25 | 26 | @disjoint_base 27 | class G: ... @@ -50,7 +50,8 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict 35 | ): ... 36 | 37 | # fmt: on -38 | class Foo(range, str): ... # error: [subclass-of-final-class] +38 | # error: [invalid-generic-class] +39 | class Foo(range, str): ... # error: [subclass-of-final-class] ``` # Diagnostics @@ -126,7 +127,7 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 22 | | ): ... | |_^ Bases `D` and `str` cannot be combined in multiple inheritance 23 | -24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] +24 | class F(int, bytes, bytearray): ... # error: [instance-layout-conflict] | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts --> src/mdtest_snippet.py:20:5 @@ -151,8 +152,8 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to | 22 | ): ... 23 | -24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `str`, `bytes` and `bytearray` cannot be combined in multiple inheritance +24 | class F(int, bytes, bytearray): ... # error: [instance-layout-conflict] + | ^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `bytes` and `bytearray` cannot be combined in multiple inheritance 25 | 26 | @disjoint_base | @@ -161,11 +162,10 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | 22 | ): ... 23 | -24 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] - | --- --- ----- --------- `bytearray` instances have a distinct memory layout because of the way `bytearray` is implemented in a C extension - | | | | - | | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension - | | `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension +24 | class F(int, bytes, bytearray): ... # error: [instance-layout-conflict] + | --- ----- --------- `bytearray` instances have a distinct memory layout because of the way `bytearray` is implemented in a C extension + | | | + | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension 25 | 26 | @disjoint_base @@ -203,12 +203,29 @@ info: rule `instance-layout-conflict` is enabled by default ``` +``` +error[invalid-generic-class]: Inconsistent type arguments for `Sequence` among class bases + --> src/mdtest_snippet.py:39:7 + | +37 | # fmt: on +38 | # error: [invalid-generic-class] +39 | class Foo(range, str): ... # error: [subclass-of-final-class] + | ^^^^-----^^---^ + | | | + | | Later class base inherits from `Sequence[str]` + | Earlier class base inherits from `Sequence[int]` + | +info: rule `invalid-generic-class` is enabled by default + +``` + ``` error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `range` - --> src/mdtest_snippet.py:38:11 + --> src/mdtest_snippet.py:39:11 | 37 | # fmt: on -38 | class Foo(range, str): ... # error: [subclass-of-final-class] +38 | # error: [invalid-generic-class] +39 | class Foo(range, str): ... # error: [subclass-of-final-class] | ^^^^^ | info: rule `subclass-of-final-class` is enabled by default diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut\342\200\246_-_The_entire_class_hie\342\200\246_(5e8fca10d966c36e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut\342\200\246_-_The_entire_class_hie\342\200\246_(5e8fca10d966c36e).snap" index 74ddba4e0bf4f..d044e4681f4b7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut\342\200\246_-_The_entire_class_hie\342\200\246_(5e8fca10d966c36e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut\342\200\246_-_The_entire_class_hie\342\200\246_(5e8fca10d966c36e).snap" @@ -1,6 +1,5 @@ --- source: crates/ty_test/src/lib.rs -assertion_line: 623 expression: snapshot --- diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index 99b939deaf882..ddba230a5140c 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -388,8 +388,8 @@ static_assert(not is_disjoint_from(tuple[F1, ...], tuple[F2, ...])) static_assert(not is_disjoint_from(tuple[N1, ...], tuple[N2, ...])) ``` -We currently model tuple types to _not_ be disjoint from arbitrary instance types, because we allow -for the possibility of `tuple` to be subclassed +We model tuple types to _not_ be disjoint from arbitrary instance types, because we allow for the +possibility of `tuple` to be subclassed. ```py class C: ... @@ -399,17 +399,14 @@ static_assert(not is_disjoint_from(tuple[int, str], C)) class CommonSubtype(tuple[int, str], C): ... ``` -Note: This is inconsistent with the fact that we model heterogeneous tuples to be disjoint from -other heterogeneous tuples above: +However, we model heterogeneous tuples to be disjoint from other heterogeneous tuples. To reconcile +these two things, we explicitly ban two differently specialized heterogeneous tuples from coexisting +in the same MRO: ```py class I1(tuple[F1, F2]): ... class I2(tuple[F2, F1]): ... - -# TODO -# This is a subtype of both `tuple[F1, F2]` and `tuple[F2, F1]`, so those two heterogeneous tuples -# should not be disjoint from each other (see conflicting test above). -class CommonSubtypeOfTuples(I1, I2): ... +class CommonSubtypeOfTuples(I1, I2): ... # error: [invalid-generic-class] ``` ## Truthiness diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 66bc5c025b545..f3e84099b5179 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -132,8 +132,8 @@ use crate::types::typed_dict::{ use crate::types::visitor::find_over_type; use crate::types::{ BoundTypeVarIdentity, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, - CallableTypeKind, ClassType, DataclassParams, DynamicType, InternedConstraintSet, InternedType, - IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, + CallableTypeKind, ClassType, DataclassParams, DynamicType, GenericAlias, InternedConstraintSet, + InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, LiteralValueType, LiteralValueTypeKind, ManualPEP695TypeAliasType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, @@ -1188,6 +1188,133 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &disjoint_bases, ); } + + // Check for inconsistent specializations of the same generic + // base class. This detects when different explicit bases + // contribute conflicting specializations of a common generic + // ancestor to the MRO. For example: + // + // class Grandparent(Generic[T1, T2]): ... + // class Parent(Grandparent[T1, T2]): ... + // class BadChild(Parent[T1, T2], Grandparent[T2, T1]): ... # Error + let explicit_bases = class.explicit_bases(self.db()); + let can_annotate_bases = || { + class_node.bases().len() == explicit_bases.len() + && !class_node.bases().iter().any(ast::Expr::is_starred_expr) + }; + + // Maps each generic ancestor's class literal to the first + // specialization seen and the index of the explicit base it + // came from. + let mut ancestor_specs = + FxHashMap::, (GenericAlias<'db>, usize)>::default(); + + 'outer: for (i, base) in explicit_bases.iter().enumerate() { + let base_class = match base { + Type::GenericAlias(c) => ClassType::Generic(*c), + Type::ClassLiteral(c) if c.generic_context(self.db()).is_none() => { + ClassType::NonGeneric(*c) + } + _ => continue, + }; + + for supercls in base_class.iter_mro(self.db()) { + let ClassBase::Class(ClassType::Generic(supercls_alias)) = supercls + else { + continue; + }; + let origin = supercls_alias.origin(self.db()); + + if let Some(&(earlier_alias, earlier_idx)) = ancestor_specs.get(&origin) + { + if earlier_idx != i + && earlier_alias + .specialization(self.db()) + .types(self.db()) + .iter() + .zip( + supercls_alias + .specialization(self.db()) + .types(self.db()), + ) + .any(|(t1, t2)| { + !t1.is_dynamic() && !t2.is_dynamic() && t1 != t2 + }) + { + let Some(builder) = self.context.report_lint( + &INVALID_GENERIC_CLASS, + class.header_range(self.db()), + ) else { + break 'outer; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Inconsistent type arguments for `{}` among class bases", + origin.name(self.db()) + )); + + let later_is_direct = matches!( + base, + Type::GenericAlias(a) + if a.origin(self.db()) == origin + ); + + if can_annotate_bases() { + diagnostic.annotate( + self.context + .secondary(&class_node.bases()[earlier_idx]) + .message(format_args!( + "Earlier class base inherits from `{}`", + earlier_alias.display(self.db()) + )), + ); + let later_annotation = + self.context.secondary(&class_node.bases()[i]); + diagnostic.annotate(if later_is_direct { + later_annotation.message(format_args!( + "Later class base is `{}`", + supercls_alias.display(self.db()) + )) + } else { + later_annotation.message(format_args!( + "Later class base inherits from `{}`", + supercls_alias.display(self.db()) + )) + }); + } else { + diagnostic.info(format_args!( + "Earlier class base inherits from `{}`", + earlier_alias.display(self.db()) + )); + if later_is_direct { + diagnostic.info(format_args!( + "Later class base is `{}`", + supercls_alias.display(self.db()) + )); + } else { + diagnostic.info(format_args!( + "Later class base inherits from `{}`", + supercls_alias.display(self.db()) + )); + } + } + diagnostic.set_concise_message(format_args!( + "Inconsistent type arguments: class cannot \ + inherit from both `{}` and `{}`", + supercls_alias.display(self.db()), + earlier_alias.display(self.db()) + )); + break 'outer; + } + } else if !supercls_alias + .specialization(self.db()) + .types(self.db()) + .iter() + .all(Type::is_dynamic) + { + ancestor_specs.insert(origin, (supercls_alias, i)); + } + } + } } } From 329b713230a991b88abc487a09e9433aad629a59 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 28 Feb 2026 15:36:05 +0000 Subject: [PATCH 137/261] [ty] Fix inference of `t.__mro__` if `t` is an instance of `type[Any]` (#23632) ## Summary I started off this PR trying to fix these conformance-suite assertions: - https://github.com/python/typing/blob/6b1a64cfbfab5e07a28fb5a811011449cb60b04b/conformance/tests/specialtypes_type.py#L102 - https://github.com/python/typing/blob/6b1a64cfbfab5e07a28fb5a811011449cb60b04b/conformance/tests/specialtypes_type.py#L110 But, alas... after starting on the PR, I realised that (as with many of the conformance-suite assertions regarding `type[]` types), I disagreed with the assertions being made! If `type[Any]` is equivalent to `type & Any` (and I [believe it is!](https://github.com/astral-sh/ty/issues/222)), then the inferred type of `t.__mro__` where `t: type[Any]` should not be `tuple[type, ...]`; it should be `tuple[type, ...] & Any`. Anyway, this PR improves semantics, I think, even if it will sadly not improve our conformance score. EDIT: oh, huh, it actually improves our conformance score anyway? I guess the error code changes from `type-assertion-failure` to `assert-type-unspellable-subtype`? Nice. ## Test Plan mdtests --------- Co-authored-by: Claude --- .../resources/mdtest/call/type.md | 14 +++++++ crates/ty_python_semantic/src/types.rs | 41 ++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 60b85b122bbec..eea1658a4e740 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -490,6 +490,20 @@ reveal_type(T.__bases__) # revealed: tuple[type, ...] reveal_type(T.__mro__) # revealed: tuple[type, ...] ``` +`type[Any]` and `type[Unknown]` are gradual forms with an unknown metaclass that is at least `type`. +Attributes defined as data descriptors on `type` (like `__mro__`) resolve to their declared types +intersected with `Unknown`, reflecting uncertainty about whether the unknown metaclass overrides +them: + +```py +from typing import Any +from ty_extensions import Unknown + +def f(a: type[Any], b: type[Unknown]): + reveal_type(a.__mro__) # revealed: tuple[type, ...] & Any + reveal_type(b.__mro__) # revealed: tuple[type, ...] & Unknown +``` + ## Invalid calls Other numbers of arguments are invalid: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9cedb38fb8a8f..76235378fb2c3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2539,6 +2539,27 @@ impl<'db> Type<'db> { inner: Protocol::Synthesized(_), .. }) => self.instance_member(db, &name), + + // `type[Any]` (or `type[Unknown]`, etc.) has an unknown metaclass, but all + // metaclasses inherit from `type`. Check `type`'s class-level attributes + // first so that data descriptors like `__mro__` and `__bases__` resolve to + // their correct types instead of collapsing to `Any`/`Unknown`. + Type::SubclassOf(subclass_of) if subclass_of.is_dynamic() => { + let type_result = KnownClass::Type + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name.as_str(), policy) + .expect("`find_name_in_mro` should return `Some` for a class literal"); + if !type_result.place.is_undefined() { + type_result + } else { + self.to_meta_type(db) + .find_name_in_mro_with_policy(db, name.as_str(), policy) + .expect( + "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", + ) + } + } + _ => self .to_meta_type(db) .find_name_in_mro_with_policy(db, name.as_str(), policy) @@ -3473,7 +3494,25 @@ impl<'db> Type<'db> { // attribute access falls back to `__getattr__`/`__getattribute__` on the // class. `try_call_dunder` adds `NO_INSTANCE_FALLBACK`, which causes the // lookup to hit the catch-all that only checks the meta-type (the metaclass). - self.fallback_to_getattr(db, &name, result, policy) + let result = self.fallback_to_getattr(db, &name, result, policy); + + // `type[Any]`/`type[Unknown]` are gradual forms with an unknown metaclass + // (which is at least `type`). Attributes resolved via `type`'s descriptors + // are intersected with the dynamic type to reflect uncertainty about + // whether the unknown metaclass overrides them. + if let Type::SubclassOf(subclass_of) = self + && let SubclassOfInner::Dynamic(dynamic) = subclass_of.subclass_of() + { + result.map_type(|ty| { + if ty.is_dynamic() { + ty + } else { + IntersectionType::from_two_elements(db, ty, Type::Dynamic(dynamic)) + } + }) + } else { + result + } } // Unlike other objects, `super` has a unique member lookup behavior. From fb70de618e07e9740d4ea6c78ebd47827cbcfddf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 01:26:41 +0000 Subject: [PATCH 138/261] [ty] Sync vendored typeshed stubs (#23642) Close and reopen this PR to trigger CI --------- Co-authored-by: typeshedbot <> --- crates/ty_vendored/vendor/typeshed/README.md | 2 +- .../vendor/typeshed/source_commit.txt | 2 +- .../vendor/typeshed/stdlib/_thread.pyi | 34 ++++-- .../vendor/typeshed/stdlib/abc.pyi | 4 +- .../vendor/typeshed/stdlib/asyncio/tasks.pyi | 6 - .../typeshed/stdlib/logging/__init__.pyi | 43 +++++-- .../stdlib/multiprocessing/managers.pyi | 23 ++++ .../stdlib/multiprocessing/queues.pyi | 12 +- .../vendor/typeshed/stdlib/opcode.pyi | 4 +- .../vendor/typeshed/stdlib/pickle.pyi | 6 +- .../vendor/typeshed/stdlib/types.pyi | 2 +- .../vendor/typeshed/stdlib/typing.pyi | 114 +++++++++--------- 12 files changed, 156 insertions(+), 96 deletions(-) diff --git a/crates/ty_vendored/vendor/typeshed/README.md b/crates/ty_vendored/vendor/typeshed/README.md index 4bf78a8300626..dae1780a41273 100644 --- a/crates/ty_vendored/vendor/typeshed/README.md +++ b/crates/ty_vendored/vendor/typeshed/README.md @@ -38,7 +38,7 @@ you can install the type stubs using $ pip install types-html5lib types-requests ``` -These PyPI packages follow [PEP 561](http://www.python.org/dev/peps/pep-0561/) +These PyPI packages follow [the typing spec standards](https://typing.python.org/en/latest/spec/distributing.html) and are automatically released (up to once a day) by [typeshed internal machinery](https://github.com/typeshed-internal/stub_uploader). diff --git a/crates/ty_vendored/vendor/typeshed/source_commit.txt b/crates/ty_vendored/vendor/typeshed/source_commit.txt index 886575caaf814..d1119d9c0ed43 100644 --- a/crates/ty_vendored/vendor/typeshed/source_commit.txt +++ b/crates/ty_vendored/vendor/typeshed/source_commit.txt @@ -1 +1 @@ -1b3cec156330a93f6bb22b6636bca38c27f8f721 +843c1fd5a148da85e523c1b4ee680226f89986aa diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi index dec47ab06b114..e1de7154ba29c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi @@ -9,7 +9,7 @@ from collections.abc import Callable from threading import Thread from types import TracebackType from typing import Any, Final, NoReturn, final, overload -from typing_extensions import TypeVarTuple, Unpack, disjoint_base +from typing_extensions import TypeVarTuple, Unpack, deprecated, disjoint_base _Ts = TypeVarTuple("_Ts") @@ -119,13 +119,16 @@ if sys.version_info >= (3, 13): def locked(self) -> bool: """Return whether the lock is in the locked state.""" - def acquire_lock(self, blocking: bool = True, timeout: float = -1) -> bool: + @deprecated("Obsolete synonym. Use `acquire()` instead.") + def acquire_lock(self, blocking: bool = True, timeout: float = -1) -> bool: # undocumented """An obsolete synonym of acquire().""" - def release_lock(self) -> None: + @deprecated("Obsolete synonym. Use `release()` instead.") + def release_lock(self) -> None: # undocumented """An obsolete synonym of release().""" - def locked_lock(self) -> bool: + @deprecated("Obsolete synonym. Use `locked()` instead.") + def locked_lock(self) -> bool: # undocumented """An obsolete synonym of locked().""" def __enter__(self) -> bool: @@ -180,7 +183,8 @@ else: Return whether the lock is in the locked state. """ - def acquire_lock(self, blocking: bool = True, timeout: float = -1) -> bool: + @deprecated("Obsolete synonym. Use `acquire()` instead.") + def acquire_lock(self, blocking: bool = True, timeout: float = -1) -> bool: # undocumented """acquire(blocking=True, timeout=-1) -> bool (acquire_lock() is an obsolete synonym) @@ -192,7 +196,8 @@ else: The blocking operation is interruptible. """ - def release_lock(self) -> None: + @deprecated("Obsolete synonym. Use `release()` instead.") + def release_lock(self) -> None: # undocumented """release() (release_lock() is an obsolete synonym) @@ -201,7 +206,8 @@ else: but it needn't be locked by the same thread that unlocks it. """ - def locked_lock(self) -> bool: + @deprecated("Obsolete synonym. Use `locked()` instead.") + def locked_lock(self) -> bool: # undocumented """locked() -> bool (locked_lock() is an obsolete synonym) @@ -245,14 +251,14 @@ def start_new_thread(function: Callable[[Unpack[_Ts]], object], args: tuple[Unpa @overload def start_new_thread(function: Callable[..., object], args: tuple[Any, ...], kwargs: dict[str, Any], /) -> int: ... - -# Obsolete synonym for start_new_thread() @overload -def start_new(function: Callable[[Unpack[_Ts]], object], args: tuple[Unpack[_Ts]], /) -> int: +@deprecated("Obsolete synonym. Use `start_new_thread()` instead.") +def start_new(function: Callable[[Unpack[_Ts]], object], args: tuple[Unpack[_Ts]], /) -> int: # undocumented """An obsolete synonym of start_new_thread().""" @overload -def start_new(function: Callable[..., object], args: tuple[Any, ...], kwargs: dict[str, Any], /) -> int: ... +@deprecated("Obsolete synonym. Use `start_new_thread()` instead.") +def start_new(function: Callable[..., object], args: tuple[Any, ...], kwargs: dict[str, Any], /) -> int: ... # undocumented if sys.version_info >= (3, 10): def interrupt_main(signum: signal.Signals = signal.SIGINT, /) -> None: @@ -277,7 +283,8 @@ def exit() -> NoReturn: thread to exit silently unless the exception is caught. """ -def exit_thread() -> NoReturn: # Obsolete synonym for exit() +@deprecated("Obsolete synonym. Use `exit()` instead.") +def exit_thread() -> NoReturn: # undocumented """An obsolete synonym of exit().""" def allocate_lock() -> LockType: @@ -285,7 +292,8 @@ def allocate_lock() -> LockType: information about locks. """ -def allocate() -> LockType: # Obsolete synonym for allocate_lock() +@deprecated("Obsolete synonym. Use `allocate_lock()` instead.") +def allocate() -> LockType: # undocumented """An obsolete synonym of allocate_lock().""" def get_ident() -> int: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi index 04202fae9444c..8fbd4974d320d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi @@ -37,10 +37,10 @@ class ABCMeta(type): mcls: type[_typeshed.Self], name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any ) -> _typeshed.Self: ... - def __instancecheck__(cls: ABCMeta, instance: Any) -> bool: + def __instancecheck__(cls: ABCMeta, instance: Any, /) -> bool: """Override for isinstance(instance, cls).""" - def __subclasscheck__(cls: ABCMeta, subclass: type) -> bool: + def __subclasscheck__(cls: ABCMeta, subclass: type, /) -> bool: """Override for issubclass(subclass, cls).""" def _dump_registry(cls: ABCMeta, file: SupportsWrite[str] | None = None) -> None: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi index 2327b442cbbae..11a2917ca379c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi @@ -609,7 +609,6 @@ else: """ if sys.version_info >= (3, 11): - @overload async def wait( fs: Iterable[_FT], *, timeout: float | None = None, return_when: str = "ALL_COMPLETED" ) -> tuple[set[_FT], set[_FT]]: @@ -627,11 +626,6 @@ if sys.version_info >= (3, 11): when the timeout occurs are returned in the second set. """ - @overload - async def wait( - fs: Iterable[Task[_T]], *, timeout: float | None = None, return_when: str = "ALL_COMPLETED" - ) -> tuple[set[Task[_T]], set[Task[_T]]]: ... - elif sys.version_info >= (3, 10): @overload async def wait( # type: ignore[overload-overlap] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi index a79718923df1c..2491154ba6843 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi @@ -1304,19 +1304,15 @@ def makeLogRecord(dict: Mapping[str, object]) -> LogRecord: instance. """ +@overload # handlers is non-None def basicConfig( *, - filename: StrPath | None = ..., - filemode: str = ..., - format: str = ..., - datefmt: str | None = ..., - style: _FormatStyle = ..., - level: _Level | None = ..., - stream: SupportsWrite[str] | None = ..., - handlers: Iterable[Handler] | None = ..., - force: bool | None = ..., - encoding: str | None = ..., - errors: str | None = ..., + format: str = ..., # default value depends on the value of `style` + datefmt: str | None = None, + style: _FormatStyle = "%", + level: _Level | None = None, + handlers: Iterable[Handler], + force: bool | None = False, ) -> None: """ Do basic configuration for the logging system. @@ -1386,6 +1382,31 @@ def basicConfig( Added the ``encoding`` and ``errors`` parameters. """ +@overload # handlers is None, filename is passed (but possibly None) +def basicConfig( + *, + filename: StrPath | None, + filemode: str = "a", + format: str = ..., # default value depends on the value of `style` + datefmt: str | None = None, + style: _FormatStyle = "%", + level: _Level | None = None, + handlers: None = None, + force: bool | None = False, + encoding: str | None = None, + errors: str | None = "backslashreplace", +) -> None: ... +@overload # handlers is None, filename is not passed +def basicConfig( + *, + format: str = ..., # default value depends on the value of `style` + datefmt: str | None = None, + style: _FormatStyle = "%", + level: _Level | None = None, + stream: SupportsWrite[str] | None = None, + handlers: None = None, + force: bool | None = False, +) -> None: ... def shutdown(handlerList: Sequence[Any] = ...) -> None: # handlerList is undocumented """ Perform any cleanup actions in the logging system (e.g. flushing diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi index b473615663bb6..2b574e911b9f6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi @@ -26,6 +26,8 @@ from .util import Finalize as _Finalize __all__ = ["BaseManager", "SyncManager", "BaseProxy", "Token", "SharedMemoryManager"] _T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") _KT = TypeVar("_KT") _VT = TypeVar("_VT") _S = TypeVar("_S") @@ -114,6 +116,25 @@ if sys.version_info >= (3, 13): def keys(self) -> list[_KT]: ... # type: ignore[override] def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override] def values(self) -> list[_VT]: ... # type: ignore[override] + if sys.version_info >= (3, 14): + # Next methods are copied from builtins.dict + @overload + def fromkeys(self, iterable: Iterable[_T], value: None = None, /) -> dict[_T, Any | None]: ... + @overload + def fromkeys(self, iterable: Iterable[_T], value: _S, /) -> dict[_T, _S]: ... + def __reversed__(self) -> Iterator[_KT]: ... + @overload + def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload # type: ignore[misc] + def __ior__(self, value: SupportsKeysAndGetItem[_KT, _VT], /) -> Self: ... + @overload + def __ior__(self, value: Iterable[tuple[_KT, _VT]], /) -> Self: ... class DictProxy(_BaseDictProxy[_KT, _VT]): def __class_getitem__(cls, args: Any, /) -> GenericAlias: @@ -217,6 +238,8 @@ class BaseListProxy(BaseProxy, MutableSequence[_T]): def insert(self, index: SupportsIndex, object: _T, /) -> None: ... def remove(self, value: _T, /) -> None: ... if sys.version_info >= (3, 14): + # Next methods are copied from builtins.list + def clear(self) -> None: ... def copy(self) -> list[_T]: ... # Use BaseListProxy[SupportsRichComparisonT] for the first overload rather than [SupportsRichComparison] # to work around invariance diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi index dfdeab7538166..1cd6c1037d3cf 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi @@ -1,15 +1,21 @@ import sys from types import GenericAlias -from typing import Any, Generic, TypeVar +from typing import Any, Generic, NewType, TypeVar __all__ = ["Queue", "SimpleQueue", "JoinableQueue"] _T = TypeVar("_T") +_QueueState = NewType("_QueueState", object) +_JoinableQueueState = NewType("_JoinableQueueState", object) +_SimpleQueueState = NewType("_SimpleQueueState", object) + class Queue(Generic[_T]): # FIXME: `ctx` is a circular dependency and it's not actually optional. # It's marked as such to be able to use the generic Queue in __init__.pyi. def __init__(self, maxsize: int = 0, *, ctx: Any = ...) -> None: ... + def __getstate__(self) -> _QueueState: ... + def __setstate__(self, state: _QueueState) -> None: ... def put(self, obj: _T, block: bool = True, timeout: float | None = None) -> None: ... def get(self, block: bool = True, timeout: float | None = None) -> _T: ... def qsize(self) -> int: ... @@ -28,6 +34,8 @@ class Queue(Generic[_T]): """ class JoinableQueue(Queue[_T]): + def __getstate__(self) -> _JoinableQueueState: ... # type: ignore[override] + def __setstate__(self, state: _JoinableQueueState) -> None: ... # type: ignore[override] def task_done(self) -> None: ... def join(self) -> None: ... @@ -35,6 +43,8 @@ class SimpleQueue(Generic[_T]): def __init__(self, *, ctx: Any = ...) -> None: ... def close(self) -> None: ... def empty(self) -> bool: ... + def __getstate__(self) -> _SimpleQueueState: ... + def __setstate__(self, state: _SimpleQueueState) -> None: ... def get(self) -> _T: ... def put(self, obj: _T) -> None: ... def __class_getitem__(cls, item: Any, /) -> GenericAlias: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/opcode.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/opcode.pyi index 080a968911290..7329a055b7472 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/opcode.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/opcode.pyi @@ -46,8 +46,8 @@ if sys.version_info >= (3, 13): opname: Final[list[str]] opmap: Final[dict[str, int]] -HAVE_ARGUMENT: Final = 43 -EXTENDED_ARG: Final = 69 +HAVE_ARGUMENT: Final[int] +EXTENDED_ARG: Final[int] def stack_effect(opcode: int, oparg: int | None = None, /, *, jump: bool | None = None) -> int: """Compute the stack effect of the opcode.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pickle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pickle.pyi index 1348b91018be7..70f999197081e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pickle.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pickle.pyi @@ -23,6 +23,7 @@ Misc variables: """ +import sys from _pickle import ( PickleError as PickleError, Pickler as Pickler, @@ -128,7 +129,10 @@ __all__ = [ ] HIGHEST_PROTOCOL: Final = 5 -DEFAULT_PROTOCOL: Final = 5 +if sys.version_info >= (3, 14): + DEFAULT_PROTOCOL: Final = 5 +else: + DEFAULT_PROTOCOL: Final = 4 bytes_types: tuple[type[Any], ...] # undocumented diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi index 47437e7432433..b6b9faa96d574 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi @@ -1008,7 +1008,7 @@ if sys.version_info >= (3, 10): def __hash__(self) -> int: ... # you can only subscript a `UnionType` instance if at least one of the elements # in the union is a generic alias instance that has a non-empty `__parameters__` - def __getitem__(self, parameters: Any) -> object: + def __getitem__(self, parameters: Any, /) -> object: """Return self[key].""" if sys.version_info >= (3, 13): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi index d3948fb35e80c..041819d57e7a5 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi @@ -1571,12 +1571,12 @@ class Sequence(Reversible[_T_co], Collection[_T_co]): @overload @abstractmethod - def __getitem__(self, index: int) -> _T_co: ... + def __getitem__(self, index: int, /) -> _T_co: ... @overload @abstractmethod - def __getitem__(self, index: slice[int | None]) -> Sequence[_T_co]: ... + def __getitem__(self, index: slice[int | None], /) -> Sequence[_T_co]: ... # Mixin methods - def index(self, value: Any, start: int = 0, stop: int = ...) -> int: + def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: """S.index(value, [start, [stop]]) -> integer -- return first index of value. Raises ValueError if the value is not present. @@ -1584,10 +1584,10 @@ class Sequence(Reversible[_T_co], Collection[_T_co]): recommended. """ - def count(self, value: Any) -> int: + def count(self, value: Any, /) -> int: """S.count(value) -> integer -- return number of occurrences of value""" - def __contains__(self, value: object) -> bool: ... + def __contains__(self, value: object, /) -> bool: ... def __iter__(self) -> Iterator[_T_co]: ... def __reversed__(self) -> Iterator[_T_co]: ... @@ -1599,51 +1599,51 @@ class MutableSequence(Sequence[_T]): """ @abstractmethod - def insert(self, index: int, value: _T) -> None: + def insert(self, index: int, value: _T, /) -> None: """S.insert(index, value) -- insert value before index""" @overload @abstractmethod - def __getitem__(self, index: int) -> _T: ... + def __getitem__(self, index: int, /) -> _T: ... @overload @abstractmethod - def __getitem__(self, index: slice[int | None]) -> MutableSequence[_T]: ... + def __getitem__(self, index: slice[int | None], /) -> MutableSequence[_T]: ... @overload @abstractmethod - def __setitem__(self, index: int, value: _T) -> None: ... + def __setitem__(self, index: int, value: _T, /) -> None: ... @overload @abstractmethod - def __setitem__(self, index: slice[int | None], value: Iterable[_T]) -> None: ... + def __setitem__(self, index: slice[int | None], value: Iterable[_T], /) -> None: ... @overload @abstractmethod - def __delitem__(self, index: int) -> None: ... + def __delitem__(self, index: int, /) -> None: ... @overload @abstractmethod - def __delitem__(self, index: slice[int | None]) -> None: ... + def __delitem__(self, index: slice[int | None], /) -> None: ... # Mixin methods - def append(self, value: _T) -> None: + def append(self, value: _T, /) -> None: """S.append(value) -- append value to the end of the sequence""" def clear(self) -> None: """S.clear() -> None -- remove all items from S""" - def extend(self, values: Iterable[_T]) -> None: + def extend(self, values: Iterable[_T], /) -> None: """S.extend(iterable) -- extend sequence by appending elements from the iterable""" def reverse(self) -> None: """S.reverse() -- reverse *IN PLACE*""" - def pop(self, index: int = -1) -> _T: + def pop(self, index: int = -1, /) -> _T: """S.pop([index]) -> item -- remove and return item at index (default last). Raise IndexError if list is empty or index is out of range. """ - def remove(self, value: _T) -> None: + def remove(self, value: _T, /) -> None: """S.remove(value) -- remove first occurrence of value. Raise ValueError if the value is not present. """ - def __iadd__(self, values: Iterable[_T]) -> typing_extensions.Self: ... + def __iadd__(self, values: Iterable[_T], /) -> typing_extensions.Self: ... class AbstractSet(Collection[_T_co]): """A set is a finite, iterable container. @@ -1657,7 +1657,7 @@ class AbstractSet(Collection[_T_co]): """ @abstractmethod - def __contains__(self, x: object) -> bool: ... + def __contains__(self, x: object, /) -> bool: ... def _hash(self) -> int: """Compute the hash value of a set. @@ -1675,23 +1675,23 @@ class AbstractSet(Collection[_T_co]): """ # Mixin methods @classmethod - def _from_iterable(cls, it: Iterable[_S]) -> AbstractSet[_S]: + def _from_iterable(cls, it: Iterable[_S], /) -> AbstractSet[_S]: """Construct an instance of the class from any iterable input. Must override this method if the class constructor signature does not accept an iterable for an input. """ - def __le__(self, other: AbstractSet[Any]) -> bool: ... - def __lt__(self, other: AbstractSet[Any]) -> bool: ... - def __gt__(self, other: AbstractSet[Any]) -> bool: ... - def __ge__(self, other: AbstractSet[Any]) -> bool: ... - def __and__(self, other: AbstractSet[Any]) -> AbstractSet[_T_co]: ... - def __or__(self, other: AbstractSet[_T]) -> AbstractSet[_T_co | _T]: ... - def __sub__(self, other: AbstractSet[Any]) -> AbstractSet[_T_co]: ... - def __xor__(self, other: AbstractSet[_T]) -> AbstractSet[_T_co | _T]: ... - def __eq__(self, other: object) -> bool: ... - def isdisjoint(self, other: Iterable[Any]) -> bool: + def __le__(self, other: AbstractSet[Any], /) -> bool: ... + def __lt__(self, other: AbstractSet[Any], /) -> bool: ... + def __gt__(self, other: AbstractSet[Any], /) -> bool: ... + def __ge__(self, other: AbstractSet[Any], /) -> bool: ... + def __and__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ... + def __or__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... + def __sub__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ... + def __xor__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... + def __eq__(self, other: object, /) -> bool: ... + def isdisjoint(self, other: Iterable[Any], /) -> bool: """Return True if two sets have a null intersection.""" class MutableSet(AbstractSet[_T]): @@ -1707,11 +1707,11 @@ class MutableSet(AbstractSet[_T]): """ @abstractmethod - def add(self, value: _T) -> None: + def add(self, value: _T, /) -> None: """Add an element.""" @abstractmethod - def discard(self, value: _T) -> None: + def discard(self, value: _T, /) -> None: """Remove an element. Do not raise an exception if absent.""" # Mixin methods def clear(self) -> None: @@ -1720,13 +1720,13 @@ class MutableSet(AbstractSet[_T]): def pop(self) -> _T: """Return the popped value. Raise KeyError if empty.""" - def remove(self, value: _T) -> None: + def remove(self, value: _T, /) -> None: """Remove an element. If not a member, raise a KeyError.""" - def __ior__(self, it: AbstractSet[_T]) -> typing_extensions.Self: ... # type: ignore[override,misc] - def __iand__(self, it: AbstractSet[Any]) -> typing_extensions.Self: ... - def __ixor__(self, it: AbstractSet[_T]) -> typing_extensions.Self: ... # type: ignore[override,misc] - def __isub__(self, it: AbstractSet[Any]) -> typing_extensions.Self: ... + def __ior__(self, it: AbstractSet[_T], /) -> typing_extensions.Self: ... # type: ignore[override,misc] + def __iand__(self, it: AbstractSet[Any], /) -> typing_extensions.Self: ... + def __ixor__(self, it: AbstractSet[_T], /) -> typing_extensions.Self: ... # type: ignore[override,misc] + def __isub__(self, it: AbstractSet[Any], /) -> typing_extensions.Self: ... class MappingView(Sized): __slots__ = ("_mapping",) @@ -1736,36 +1736,36 @@ class MappingView(Sized): class ItemsView(MappingView, AbstractSet[tuple[_KT_co, _VT_co]], Generic[_KT_co, _VT_co]): def __init__(self, mapping: SupportsGetItemViewable[_KT_co, _VT_co]) -> None: ... # undocumented @classmethod - def _from_iterable(cls, it: Iterable[_S]) -> set[_S]: ... - def __and__(self, other: Iterable[Any]) -> set[tuple[_KT_co, _VT_co]]: ... - def __rand__(self, other: Iterable[_T]) -> set[_T]: ... - def __contains__(self, item: tuple[object, object]) -> bool: ... # type: ignore[override] + def _from_iterable(cls, it: Iterable[_S], /) -> set[_S]: ... + def __and__(self, other: Iterable[Any], /) -> set[tuple[_KT_co, _VT_co]]: ... + def __rand__(self, other: Iterable[_T], /) -> set[_T]: ... + def __contains__(self, item: tuple[object, object], /) -> bool: ... # type: ignore[override] def __iter__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ... - def __or__(self, other: Iterable[_T]) -> set[tuple[_KT_co, _VT_co] | _T]: ... - def __ror__(self, other: Iterable[_T]) -> set[tuple[_KT_co, _VT_co] | _T]: ... - def __sub__(self, other: Iterable[Any]) -> set[tuple[_KT_co, _VT_co]]: ... - def __rsub__(self, other: Iterable[_T]) -> set[_T]: ... - def __xor__(self, other: Iterable[_T]) -> set[tuple[_KT_co, _VT_co] | _T]: ... - def __rxor__(self, other: Iterable[_T]) -> set[tuple[_KT_co, _VT_co] | _T]: ... + def __or__(self, other: Iterable[_T], /) -> set[tuple[_KT_co, _VT_co] | _T]: ... + def __ror__(self, other: Iterable[_T], /) -> set[tuple[_KT_co, _VT_co] | _T]: ... + def __sub__(self, other: Iterable[Any], /) -> set[tuple[_KT_co, _VT_co]]: ... + def __rsub__(self, other: Iterable[_T], /) -> set[_T]: ... + def __xor__(self, other: Iterable[_T], /) -> set[tuple[_KT_co, _VT_co] | _T]: ... + def __rxor__(self, other: Iterable[_T], /) -> set[tuple[_KT_co, _VT_co] | _T]: ... class KeysView(MappingView, AbstractSet[_KT_co]): def __init__(self, mapping: Viewable[_KT_co]) -> None: ... # undocumented @classmethod - def _from_iterable(cls, it: Iterable[_S]) -> set[_S]: ... - def __and__(self, other: Iterable[Any]) -> set[_KT_co]: ... - def __rand__(self, other: Iterable[_T]) -> set[_T]: ... - def __contains__(self, key: object) -> bool: ... + def _from_iterable(cls, it: Iterable[_S], /) -> set[_S]: ... + def __and__(self, other: Iterable[Any], /) -> set[_KT_co]: ... + def __rand__(self, other: Iterable[_T], /) -> set[_T]: ... + def __contains__(self, key: object, /) -> bool: ... def __iter__(self) -> Iterator[_KT_co]: ... - def __or__(self, other: Iterable[_T]) -> set[_KT_co | _T]: ... - def __ror__(self, other: Iterable[_T]) -> set[_KT_co | _T]: ... - def __sub__(self, other: Iterable[Any]) -> set[_KT_co]: ... - def __rsub__(self, other: Iterable[_T]) -> set[_T]: ... - def __xor__(self, other: Iterable[_T]) -> set[_KT_co | _T]: ... - def __rxor__(self, other: Iterable[_T]) -> set[_KT_co | _T]: ... + def __or__(self, other: Iterable[_T], /) -> set[_KT_co | _T]: ... + def __ror__(self, other: Iterable[_T], /) -> set[_KT_co | _T]: ... + def __sub__(self, other: Iterable[Any], /) -> set[_KT_co]: ... + def __rsub__(self, other: Iterable[_T], /) -> set[_T]: ... + def __xor__(self, other: Iterable[_T], /) -> set[_KT_co | _T]: ... + def __rxor__(self, other: Iterable[_T], /) -> set[_KT_co | _T]: ... class ValuesView(MappingView, Collection[_VT_co]): def __init__(self, mapping: SupportsGetItemViewable[Any, _VT_co]) -> None: ... # undocumented - def __contains__(self, value: object) -> bool: ... + def __contains__(self, value: object, /) -> bool: ... def __iter__(self) -> Iterator[_VT_co]: ... # note for Mapping.get and MutableMapping.pop and MutableMapping.setdefault From 033a4fb64c1ba07fb425275e3a67641c431e2a02 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 1 Mar 2026 12:37:22 +0000 Subject: [PATCH 139/261] [ty] Add more ParamSpec validation for `P.args` and `P.kwargs` (#23640) --- .../mdtest/generics/legacy/paramspec.md | 39 ++++- .../mdtest/generics/pep695/paramspec.md | 43 ++++-- .../src/types/infer/builder.rs | 112 +++++++++++++++ .../infer/builder/paramspec_validation.rs | 135 ++++++++++++++++++ 4 files changed, 313 insertions(+), 16 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 63465627c0bdf..f29db13a19e8a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -201,15 +201,23 @@ def foo1(c: Callable[P, int]) -> None: **kwargs: P.args, ) -> None: ... - # TODO: error + # error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`" def nested3(*args: P.args) -> None: ... - # TODO: error + # error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`" def nested4(**kwargs: P.kwargs) -> None: ... - # TODO: error + # error: [invalid-paramspec] "No parameters may appear between `*args: P.args` and `**kwargs: P.kwargs`" def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ... + # error: [invalid-paramspec] "`P.args` is only valid for annotating `*args`" + def nested6(x: P.args) -> None: ... + def nested7( + *args: P.args, + # error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`" + **kwargs: int, + ) -> None: ... + # TODO: error def bar1(*args: P.args, **kwargs: P.kwargs) -> None: pass @@ -223,17 +231,17 @@ And, they need to be used together. ```py def foo2(c: Callable[P, int]) -> None: - # TODO: error + # error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`" def nested1(*args: P.args) -> None: ... - # TODO: error + # error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`" def nested2(**kwargs: P.kwargs) -> None: ... class Foo2: - # TODO: error + # error: [invalid-paramspec] "`P.args` is only valid for annotating `*args` function parameters" args: P.args - # TODO: error + # error: [invalid-paramspec] "`P.kwargs` is only valid for annotating `**kwargs` function parameters" kwargs: P.kwargs ``` @@ -252,6 +260,23 @@ class Foo3(Generic[P]): ) -> None: ... ``` +Error messages for `invalid-paramspec` also use the actual parameter names: + +```py +def bar(c: Callable[P, int]) -> None: + # error: [invalid-paramspec] "`*my_args: P.args` must be accompanied by `**my_kwargs: P.kwargs`" + def f1(*my_args: P.args, **my_kwargs: int) -> None: ... + + # error: [invalid-paramspec] "`*positional: P.args` must be accompanied by `**kwargs: P.kwargs`" + def f2(*positional: P.args) -> None: ... + + # error: [invalid-paramspec] "`**keyword: P.kwargs` must be accompanied by `*args: P.args`" + def f3(**keyword: P.kwargs) -> None: ... + + # error: [invalid-paramspec] "No parameters may appear between `*a: P.args` and `**kw: P.kwargs`" + def f4(*a: P.args, x: int, **kw: P.kwargs) -> None: ... +``` + ## Specializing generic classes explicitly ```py diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 2c75fa153f64d..7b0d89f82e0ad 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -103,31 +103,39 @@ def foo[**P](c: Callable[P, int]) -> None: # error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?" def nested2(*args: P.kwargs, **kwargs: P.args) -> None: ... - # TODO: error + # error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`" def nested3(*args: P.args) -> None: ... - # TODO: error + # error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`" def nested4(**kwargs: P.kwargs) -> None: ... - # TODO: error + # error: [invalid-paramspec] "No parameters may appear between `*args: P.args` and `**kwargs: P.kwargs`" def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ... + + # error: [invalid-paramspec] "`P.args` is only valid for annotating `*args`" + def nested6(x: P.args) -> None: ... + def nested7( + *args: P.args, + # error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`" + **kwargs: int, + ) -> None: ... ``` And, they need to be used together. ```py def foo[**P](c: Callable[P, int]) -> None: - # TODO: error + # error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`" def nested1(*args: P.args) -> None: ... - # TODO: error + # error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`" def nested2(**kwargs: P.kwargs) -> None: ... class Foo[**P]: - # TODO: error + # error: [invalid-paramspec] "`P.args` is only valid for annotating `*args` function parameters" args: P.args - # TODO: error + # error: [invalid-paramspec] "`P.kwargs` is only valid for annotating `**kwargs` function parameters" kwargs: P.kwargs ``` @@ -146,15 +154,32 @@ class Foo3[**P]: ) -> None: ... ``` +Error messages for `invalid-paramspec` also use the actual parameter names: + +```py +def bar[**P](c: Callable[P, int]) -> None: + # error: [invalid-paramspec] "`*my_args: P.args` must be accompanied by `**my_kwargs: P.kwargs`" + def f1(*my_args: P.args, **my_kwargs: int) -> None: ... + + # error: [invalid-paramspec] "`*positional: P.args` must be accompanied by `**kwargs: P.kwargs`" + def f2(*positional: P.args) -> None: ... + + # error: [invalid-paramspec] "`**keyword: P.kwargs` must be accompanied by `*args: P.args`" + def f3(**keyword: P.kwargs) -> None: ... + + # error: [invalid-paramspec] "No parameters may appear between `*a: P.args` and `**kw: P.kwargs`" + def f4(*a: P.args, x: int, **kw: P.kwargs) -> None: ... +``` + It isn't allowed to annotate an instance attribute either: ```py class Foo4[**P]: def __init__(self, fn: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: self.fn = fn - # TODO: error + # error: [invalid-paramspec] "`P.args` is only valid for annotating `*args` function parameters" self.args: P.args = args - # TODO: error + # error: [invalid-paramspec] "`P.kwargs` is only valid for annotating `**kwargs` function parameters" self.kwargs: P.kwargs = kwargs ``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f3e84099b5179..20ef2cc86e8a3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -118,6 +118,7 @@ use crate::types::generics::{ GenericContext, InferableTypeVars, SpecializationBuilder, bind_typevar, enclosing_generic_contexts, typing_self, }; +use crate::types::infer::builder::paramspec_validation::validate_paramspec_components; use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function}; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; @@ -149,6 +150,7 @@ use crate::unpack::{EvaluationMode, UnpackPosition}; use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet, Program}; mod annotation_expression; +mod paramspec_validation; mod type_expression; /// Whether the intersection type is on the left or right side of the comparison. @@ -3176,6 +3178,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for parameter in &function.parameters { self.infer_definition(parameter); } + + validate_paramspec_components(&self.context, &function.parameters, |expr| { + self.file_expression_type(expr) + }); + self.infer_body(&function.body); if let Some(returns) = function.returns.as_deref() { @@ -3668,6 +3675,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let default_expr = default.as_ref(); if let Some(annotation) = parameter.annotation.as_ref() { let declared_ty = self.file_expression_type(annotation); + + // P.args and P.kwargs are only valid as annotations on *args and **kwargs, + // not on regular parameters. + if let Type::TypeVar(typevar) = declared_ty + && typevar.is_paramspec(self.db()) + && let Some(attr) = typevar.paramspec_attr(self.db()) + { + let name = typevar.name(self.db()); + let (attr_name, variadic) = match attr { + ParamSpecAttrKind::Args => ("args", "*args"), + ParamSpecAttrKind::Kwargs => ("kwargs", "**kwargs"), + }; + if let Some(builder) = self + .context + .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) + { + builder.into_diagnostic(format_args!( + "`{name}.{attr_name}` is only valid for annotating `{variadic}`", + )); + } + } + if let Some(default_expr) = default_expr { let default_expr = default_expr.as_ref(); let default_ty = self.file_expression_type(default_expr); @@ -9224,6 +9253,49 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + // P.args and P.kwargs are only valid as annotations on *args and **kwargs. + if let Type::TypeVar(typevar) = annotated.inner_type() + && typevar.is_paramspec(self.db()) + && let Some(attr) = typevar.paramspec_attr(self.db()) + { + let name = typevar.name(self.db()); + let (attr_name, variadic) = match attr { + ParamSpecAttrKind::Args => ("args", "*args"), + ParamSpecAttrKind::Kwargs => ("kwargs", "**kwargs"), + }; + if let Some(builder) = self + .context + .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) + { + builder.into_diagnostic(format_args!( + "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + )); + } + } else if let ast::Expr::Attribute(attr_expr) = annotation.as_ref() + && matches!(attr_expr.attr.as_str(), "args" | "kwargs") + { + let value_ty = self.expression_type(&attr_expr.value); + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty + && typevar.is_paramspec(self.db()) + { + let name = typevar.name(self.db()); + let attr_name = &attr_expr.attr; + let variadic = if attr_name == "args" { + "*args" + } else { + "**kwargs" + }; + if let Some(builder) = self + .context + .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) + { + builder.into_diagnostic(format_args!( + "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + )); + } + } + } + let value_ty = value.as_ref().map(|value| { self.infer_maybe_standalone_expression( value, @@ -9368,6 +9440,46 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { DeferredExpressionState::from(self.defer_annotations()), ); + // P.args and P.kwargs are only valid as annotations on *args and **kwargs, + // not as variable annotations. Check both resolved type and AST form. + if let Type::TypeVar(typevar) = declared.inner_type() + && typevar.is_paramspec(self.db()) + && let Some(attr) = typevar.paramspec_attr(self.db()) + { + let name = typevar.name(self.db()); + let (attr_name, variadic) = match attr { + ParamSpecAttrKind::Args => ("args", "*args"), + ParamSpecAttrKind::Kwargs => ("kwargs", "**kwargs"), + }; + if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, annotation) { + builder.into_diagnostic(format_args!( + "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + )); + } + } else if let ast::Expr::Attribute(attr_expr) = annotation + && matches!(attr_expr.attr.as_str(), "args" | "kwargs") + { + // Also check the AST form for cases where P isn't bound (e.g., class body + // annotations). In this case, the type might not resolve to a TypeVar. + let value_ty = self.expression_type(&attr_expr.value); + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty + && typevar.is_paramspec(self.db()) + { + let name = typevar.name(self.db()); + let attr_name = &attr_expr.attr; + let variadic = if attr_name == "args" { + "*args" + } else { + "**kwargs" + }; + if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, annotation) { + builder.into_diagnostic(format_args!( + "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + )); + } + } + } + let is_pep_613_type_alias = declared.inner_type().is_typealias_special_form(); if is_pep_613_type_alias diff --git a/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs b/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs new file mode 100644 index 0000000000000..dcd2ee3f54d08 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs @@ -0,0 +1,135 @@ +use crate::types::{ParamSpecAttrKind, Type, context::InferContext, diagnostic::INVALID_PARAMSPEC}; +use ruff_python_ast as ast; +use ruff_text_size::Ranged; + +/// Validate the usage of `ParamSpec` components (`P.args` and `P.kwargs`) across all +/// parameters of a function. +/// +/// This enforces several rules from the typing spec: +/// - `P.args` and `P.kwargs` must always be used together +/// - When `*args: P.args` is present, `**kwargs: P.kwargs` must also be present (same P) +/// - No keyword-only parameters are allowed between `*args: P.args` and `**kwargs: P.kwargs` +pub(super) fn validate_paramspec_components<'db>( + context: &'db InferContext<'db, '_>, + parameters: &ast::Parameters, + infer_type: impl Fn(&ast::Expr) -> Type<'db>, +) { + let db = context.db(); + + // Extract ParamSpec info from *args annotation + let args_paramspec = parameters.vararg.as_deref().and_then(|vararg| { + let annotation = vararg.annotation()?; + let ty = infer_type(annotation); + if let Type::TypeVar(typevar) = ty + && typevar.is_paramspec(db) + && typevar.paramspec_attr(db) == Some(ParamSpecAttrKind::Args) + { + Some((typevar.without_paramspec_attr(db), annotation)) + } else { + None + } + }); + + // Extract ParamSpec info from **kwargs annotation + let kwargs_paramspec = parameters.kwarg.as_deref().and_then(|kwarg| { + let annotation = kwarg.annotation()?; + let ty = infer_type(annotation); + if let Type::TypeVar(typevar) = ty + && typevar.is_paramspec(db) + && typevar.paramspec_attr(db) == Some(ParamSpecAttrKind::Kwargs) + { + Some((typevar.without_paramspec_attr(db), annotation)) + } else { + None + } + }); + + let vararg_name = parameters.vararg.as_deref().map(|v| v.name.as_str()); + let kwarg_name = parameters.kwarg.as_deref().map(|k| k.name.as_str()); + + match (args_paramspec, kwargs_paramspec) { + // Both *args: P.args and **kwargs: P.kwargs present + (Some((args_tv, _args_annotation)), Some((kwargs_tv, kwargs_annotation))) => { + // Check they refer to the same ParamSpec + if !args_tv.is_same_typevar_as(db, kwargs_tv) { + let args_name = args_tv.name(db); + let vararg = vararg_name.unwrap_or("args"); + let kwarg = kwarg_name.unwrap_or("kwargs"); + if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, kwargs_annotation) { + builder.into_diagnostic(format_args!( + "`*{vararg}: {args_name}.args` must be accompanied \ + by `**{kwarg}: {args_name}.kwargs`", + )); + } + } else { + // Same ParamSpec - check no keyword-only params between them + if !parameters.kwonlyargs.is_empty() { + let name = args_tv.name(db); + let vararg = vararg_name.unwrap_or("args"); + let kwarg = kwarg_name.unwrap_or("kwargs"); + if let Some(builder) = + context.report_lint(&INVALID_PARAMSPEC, ¶meters.kwonlyargs[0]) + { + builder.into_diagnostic(format_args!( + "No parameters may appear between \ + `*{vararg}: {name}.args` and `**{kwarg}: {name}.kwargs`", + )); + } + } + } + } + + // *args: P.args without matching **kwargs: P.kwargs + (Some((args_tv, args_annotation)), None) => { + let name = args_tv.name(db); + let vararg = vararg_name.unwrap_or("args"); + let kwarg = kwarg_name.unwrap_or("kwargs"); + // Report on the kwarg annotation if it exists, otherwise on *args + let range = if let Some(kwarg_param) = parameters.kwarg.as_deref() { + kwarg_param + .annotation() + .map(Ranged::range) + .unwrap_or_else(|| kwarg_param.range()) + } else { + args_annotation.range() + }; + if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, range) { + builder.into_diagnostic(format_args!( + "`*{vararg}: {name}.args` must be accompanied by `**{kwarg}: {name}.kwargs`", + )); + } + } + + // **kwargs: P.kwargs without matching *args: P.args + (None, Some((kwargs_tv, kwargs_annotation))) => { + let name = kwargs_tv.name(db); + let vararg = vararg_name.unwrap_or("args"); + let kwarg = kwarg_name.unwrap_or("kwargs"); + // Report on the vararg annotation if it exists, otherwise on **kwargs + let range = if let Some(vararg_param) = parameters.vararg.as_deref() { + vararg_param + .annotation() + .map(Ranged::range) + .unwrap_or_else(|| vararg_param.range()) + } else { + kwargs_annotation.range() + }; + if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, range) { + builder.into_diagnostic(format_args!( + "`**{kwarg}: {name}.kwargs` must be accompanied by `*{vararg}: {name}.args`", + )); + } else { + // No *args at all + if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, kwargs_annotation) { + builder.into_diagnostic(format_args!( + "`**{kwarg}: {name}.kwargs` must be accompanied by \ + `*{kwarg}: {name}.args`", + )); + } + } + } + + // No ParamSpec components in either position + (None, None) => {} + } +} From 808b5213449977b998443fd5b9247e398478c386 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 1 Mar 2026 10:01:14 -0500 Subject: [PATCH 140/261] Avoid inserting redundant `None` elements in UP045 (#23459) ## Summary Closes https://github.com/astral-sh/ruff/issues/23429. --- .../test/fixtures/pyupgrade/UP045.py | 9 ++ .../pyupgrade/rules/use_pep604_annotation.rs | 67 +++++++++++--- ...er__rules__pyupgrade__tests__UP045.py.snap | 89 +++++++++++++++++++ 3 files changed, 155 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py index 96ce4e708194a..d6a9242acaf68 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py @@ -82,3 +82,12 @@ class ServiceRefOrValue: int # text ] = None + + +# Regression test for: https://github.com/astral-sh/ruff/issues/23429 +# Optional[None | X] should not produce None | None +bar: None | Optional[None | int] = None +bar: Optional[None | int] = None +bar: Optional[int | None] = None +bar: Optional[None | int | str] = None +bar: Optional[None | None] = None diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 0478594fd50f1..fc2c5ca0948d7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -1,7 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::PythonVersion; use ruff_python_ast::helpers::{pep_604_optional, pep_604_union}; -use ruff_python_ast::{self as ast, Expr}; +use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_semantic::analyze::typing::{Pep604Operator, to_pep604_operator}; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; @@ -190,17 +190,40 @@ pub(crate) fn non_pep604_annotation( } } - diagnostic.set_fix(Fix::applicable_edit( - Edit::range_replacement( - pad( - checker.generator().expr(&pep_604_optional(inner)), + // If the inner expression is a `BitOr` union that already + // contains `None`, strip it out and re-add it only at the end. + // This avoids generating `None | None` which is a runtime + // `TypeError`. For example, `Optional[None | int]` should + // become `int | None`, not `None | int | None`. + let fix_expr = if let Expr::BinOp(ast::ExprBinOp { + op: Operator::BitOr, + .. + }) = inner + { + let elements = collect_non_none(inner); + if elements.is_empty() { + // All elements were `None`; don't provide a fix. + None + } else { + Some(pep_604_optional(&pep_604_union(&elements))) + } + } else { + Some(pep_604_optional(inner)) + }; + + if let Some(fix_expr) = fix_expr { + diagnostic.set_fix(Fix::applicable_edit( + Edit::range_replacement( + pad( + checker.generator().expr(&fix_expr), + expr.range(), + checker.locator(), + ), expr.range(), - checker.locator(), ), - expr.range(), - ), - applicability, - )); + applicability, + )); + } } } } @@ -332,3 +355,27 @@ fn is_named_tuple(checker: &Checker, expr: &Expr) -> bool { fn is_optional_none(operator: Pep604Operator, slice: &Expr) -> bool { matches!(operator, Pep604Operator::Optional) && matches!(slice, Expr::NoneLiteral(_)) } + +/// Collect all non-`None` leaf elements of a chain of `BitOr` binary operations. +/// +/// For example, `a | None | b` is collected as `[a, b]`. +fn collect_non_none(expr: &Expr) -> Vec { + fn inner(expr: &Expr, elements: &mut Vec) { + if let Expr::BinOp(ast::ExprBinOp { + left, + op: Operator::BitOr, + right, + .. + }) = expr + { + inner(left, elements); + inner(right, elements); + } else if !expr.is_none_literal_expr() { + elements.push(expr.clone()); + } + } + + let mut elements = Vec::new(); + inner(expr, &mut elements); + elements +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap index 49b5ef8740776..5ba57fe970998 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap @@ -311,4 +311,93 @@ help: Convert to `X | None` - # text - ] = None 81 + foo: int | None = None +82 | +83 | +84 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429 note: This is an unsafe fix and may change runtime behavior + +UP045 [*] Use `X | None` for type annotations + --> UP045.py:89:13 + | +87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429 +88 | # Optional[None | X] should not produce None | None +89 | bar: None | Optional[None | int] = None + | ^^^^^^^^^^^^^^^^^^^^ +90 | bar: Optional[None | int] = None +91 | bar: Optional[int | None] = None + | +help: Convert to `X | None` +86 | +87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429 +88 | # Optional[None | X] should not produce None | None + - bar: None | Optional[None | int] = None +89 + bar: None | int | None = None +90 | bar: Optional[None | int] = None +91 | bar: Optional[int | None] = None +92 | bar: Optional[None | int | str] = None + +UP045 [*] Use `X | None` for type annotations + --> UP045.py:90:6 + | +88 | # Optional[None | X] should not produce None | None +89 | bar: None | Optional[None | int] = None +90 | bar: Optional[None | int] = None + | ^^^^^^^^^^^^^^^^^^^^ +91 | bar: Optional[int | None] = None +92 | bar: Optional[None | int | str] = None + | +help: Convert to `X | None` +87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429 +88 | # Optional[None | X] should not produce None | None +89 | bar: None | Optional[None | int] = None + - bar: Optional[None | int] = None +90 + bar: int | None = None +91 | bar: Optional[int | None] = None +92 | bar: Optional[None | int | str] = None +93 | bar: Optional[None | None] = None + +UP045 [*] Use `X | None` for type annotations + --> UP045.py:91:6 + | +89 | bar: None | Optional[None | int] = None +90 | bar: Optional[None | int] = None +91 | bar: Optional[int | None] = None + | ^^^^^^^^^^^^^^^^^^^^ +92 | bar: Optional[None | int | str] = None +93 | bar: Optional[None | None] = None + | +help: Convert to `X | None` +88 | # Optional[None | X] should not produce None | None +89 | bar: None | Optional[None | int] = None +90 | bar: Optional[None | int] = None + - bar: Optional[int | None] = None +91 + bar: int | None = None +92 | bar: Optional[None | int | str] = None +93 | bar: Optional[None | None] = None + +UP045 [*] Use `X | None` for type annotations + --> UP045.py:92:6 + | +90 | bar: Optional[None | int] = None +91 | bar: Optional[int | None] = None +92 | bar: Optional[None | int | str] = None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +93 | bar: Optional[None | None] = None + | +help: Convert to `X | None` +89 | bar: None | Optional[None | int] = None +90 | bar: Optional[None | int] = None +91 | bar: Optional[int | None] = None + - bar: Optional[None | int | str] = None +92 + bar: int | str | None = None +93 | bar: Optional[None | None] = None + +UP045 Use `X | None` for type annotations + --> UP045.py:93:6 + | +91 | bar: Optional[int | None] = None +92 | bar: Optional[None | int | str] = None +93 | bar: Optional[None | None] = None + | ^^^^^^^^^^^^^^^^^^^^^ + | +help: Convert to `X | None` From d3b9e6000423608a20c3ac563f0e7441decea030 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 1 Mar 2026 11:01:10 -0500 Subject: [PATCH 141/261] [ty] Move comparison logic out of `builder.rs` (#23646) ## Summary No functional changes. Just a refactor to move some of the builder code out to a separate module. Testing the waters on our appetite for this... --- crates/ty_python_semantic/src/types/infer.rs | 3 +- .../src/types/infer/builder.rs | 1037 +---------------- .../src/types/infer/comparisons.rs | 1035 ++++++++++++++++ 3 files changed, 1070 insertions(+), 1005 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/infer/comparisons.rs diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index f5e0a07c5788e..c77447049e614 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -57,9 +57,10 @@ use crate::types::{ }; use crate::unpack::Unpack; use builder::TypeInferenceBuilder; -pub(super) use builder::UnsupportedComparisonError; +pub(super) use comparisons::UnsupportedComparisonError; mod builder; +mod comparisons; #[cfg(test)] mod tests; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 20ef2cc86e8a3..f7e40d280963c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -69,7 +69,6 @@ use crate::types::class::{ }; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{InNoTypeCheck, InferContext}; -use crate::types::cyclic::CycleDetector; use crate::types::diagnostic::{ self, ABSTRACT_METHOD_IN_FINAL_CLASS, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, @@ -125,7 +124,7 @@ use crate::types::newtype::NewType; use crate::types::special_form::AliasSpec; use crate::types::subclass_of::SubclassOfInner; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; -use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleSpecBuilder, TupleType}; +use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType}; use crate::types::typed_dict::{ TypedDictAssignmentKind, TypedDictKeyAssignment, validate_typed_dict_constructor, validate_typed_dict_dict_literal, @@ -135,14 +134,14 @@ use crate::types::{ BoundTypeVarIdentity, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypeKind, ClassType, DataclassParams, DynamicType, GenericAlias, InternedConstraintSet, InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, - LintDiagnosticGuard, LiteralValueType, LiteralValueTypeKind, ManualPEP695TypeAliasType, - MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, - ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, - Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, - TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, - TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, - TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, any_over_type, binding_type, - definition_expression_type, infer_complete_scope_types, infer_scope_types, todo_type, + LintDiagnosticGuard, LiteralValueTypeKind, ManualPEP695TypeAliasType, MemberLookupPolicy, + MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, + Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, + TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, TypeVarDefaultEvaluation, + TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, + UnionType, UnionTypeInstance, any_over_type, binding_type, definition_expression_type, + infer_complete_scope_types, infer_scope_types, todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; @@ -153,12 +152,7 @@ mod annotation_expression; mod paramspec_validation; mod type_expression; -/// Whether the intersection type is on the left or right side of the comparison. -#[derive(Debug, Clone, Copy)] -enum IntersectionOn { - Left, - Right, -} +use super::comparisons::{self, BinaryComparisonVisitor}; #[derive(Debug, Clone, Copy, Eq, PartialEq)] struct TypeAndRange<'db> { @@ -188,13 +182,6 @@ impl<'db> DeclaredAndInferredType<'db> { } } -/// A [`CycleDetector`] that is used in `infer_binary_type_comparison`. -type BinaryComparisonVisitor<'db> = CycleDetector< - ast::CmpOp, - (Type<'db>, ast::CmpOp, Type<'db>), - Result, UnsupportedComparisonError<'db>>, ->; - /// We currently store one dataclass field-specifiers inline, because that covers standard /// dataclasses. attrs uses 2 specifiers, pydantic and strawberry use 3 specifiers. SQLAlchemy /// uses 7 field specifiers. We could probably store more inline if this turns out to be a @@ -15389,914 +15376,38 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let range = TextRange::new(left.start(), right.end()); - let ty = builder - .infer_binary_type_comparison( - left_ty, - *op, - right_ty, - range, - &BinaryComparisonVisitor::new(Ok(Type::bool_literal(true))), - ) - .unwrap_or_else(|error| { - report_unsupported_comparison( - &builder.context, - &error, - range, - left, - right, - left_ty, - right_ty, - ); - - match op { - // `in, not in, is, is not` always return bool instances - ast::CmpOp::In - | ast::CmpOp::NotIn - | ast::CmpOp::Is - | ast::CmpOp::IsNot => KnownClass::Bool.to_instance(builder.db()), - // Other operators can return arbitrary types - _ => Type::unknown(), - } - }); - - (ty, range) - }, - ) - } - - fn infer_binary_intersection_type_comparison( - &mut self, - intersection: IntersectionType<'db>, - op: ast::CmpOp, - other: Type<'db>, - intersection_on: IntersectionOn, - range: TextRange, - visitor: &BinaryComparisonVisitor<'db>, - ) -> Result, UnsupportedComparisonError<'db>> { - enum State<'db> { - // We have not seen any positive elements (yet) - NoPositiveElements, - // The operator was unsupported on all elements that we have seen so far. - // Contains the first error we encountered. - UnsupportedOnAllElements(UnsupportedComparisonError<'db>), - // The operator was supported on at least one positive element. - Supported, - } - - // If a comparison yields a definitive true/false answer on a (positive) part - // of an intersection type, it will also yield a definitive answer on the full - // intersection type, which is even more specific. - for pos in intersection.positive(self.db()) { - let result = match intersection_on { - IntersectionOn::Left => { - self.infer_binary_type_comparison(*pos, op, other, range, visitor) - } - IntersectionOn::Right => { - self.infer_binary_type_comparison(other, op, *pos, range, visitor) - } - }; - - if result - .ok() - .and_then(Type::as_literal_value) - .is_some_and(LiteralValueType::is_bool) - { - return result; - } - } - - // For negative contributions to the intersection type, there are only a few - // special cases that allow us to narrow down the result type of the comparison. - for neg in intersection.negative(self.db()) { - let result = match intersection_on { - IntersectionOn::Left => self - .infer_binary_type_comparison(*neg, op, other, range, visitor) - .ok(), - IntersectionOn::Right => self - .infer_binary_type_comparison(other, op, *neg, range, visitor) - .ok(), - } - .and_then(Type::as_literal_value_kind); - - match (op, result) { - (ast::CmpOp::Is, Some(LiteralValueTypeKind::Bool(true))) => { - return Ok(Type::bool_literal(false)); - } - (ast::CmpOp::IsNot, Some(LiteralValueTypeKind::Bool(false))) => { - return Ok(Type::bool_literal(true)); - } - _ => {} - } - } - - // If none of the simplifications above apply, we still need to return *some* - // result type for the comparison 'T_inter `op` T_other' (or reversed), where - // - // T_inter = P1 & P2 & ... & Pn & ~N1 & ~N2 & ... & ~Nm - // - // is the intersection type. If f(T) is the function that computes the result - // type of a `op`-comparison with `T_other`, we are interested in f(T_inter). - // Since we can't compute it exactly, we return the following approximation: - // - // f(T_inter) = f(P1) & f(P2) & ... & f(Pn) - // - // The reason for this is the following: In general, for any function 'f', the - // set f(A) & f(B) is *larger than or equal to* the set f(A & B). This means - // that we will return a type that is possibly wider than it could be, but - // never wrong. - // - // However, we do have to leave out the negative contributions. If we were to - // add a contribution like ~f(N1), we would potentially infer result types - // that are too narrow. - // - // As an example for this, consider the intersection type `int & ~Literal[1]`. - // If 'f' would be the `==`-comparison with 2, we obviously can't tell if that - // answer would be true or false, so we need to return `bool`. And indeed, we - // we have (glossing over notational details): - // - // f(int & ~1) - // = f({..., -1, 0, 2, 3, ...}) - // = {..., False, False, True, False, ...} - // = bool - // - // On the other hand, if we were to compute - // - // f(int) & ~f(1) - // = bool & ~False - // = True - // - // we would get a result type `Literal[True]` which is too narrow. - // - let mut builder = IntersectionBuilder::new(self.db()); - - builder = builder.add_positive(KnownClass::Bool.to_instance(self.db())); - - let mut state = State::NoPositiveElements; - - for pos in intersection.positive(self.db()) { - let result = match intersection_on { - IntersectionOn::Left => { - self.infer_binary_type_comparison(*pos, op, other, range, visitor) - } - IntersectionOn::Right => { - self.infer_binary_type_comparison(other, op, *pos, range, visitor) - } - }; - - match result { - Ok(ty) => { - state = State::Supported; - builder = builder.add_positive(ty); - } - Err(error) => { - match state { - State::NoPositiveElements => { - // This is the first positive element, but the operation is not supported. - // Store the error and continue. - state = State::UnsupportedOnAllElements(error); - } - State::UnsupportedOnAllElements(_) => { - // We already have an error stored, and continue to see elements on which - // the operator is not supported. Continue with the same state (only keep - // the first error). - } - State::Supported => { - // We previously saw a positive element that supported the operator, - // so the overall operation is still supported. - } - } - } - } - } - - match state { - State::Supported => Ok(builder.build()), - State::NoPositiveElements => { - // We didn't see any positive elements, check if the operation is supported on `object`: - match intersection_on { - IntersectionOn::Left => { - self.infer_binary_type_comparison(Type::object(), op, other, range, visitor) - } - IntersectionOn::Right => { - self.infer_binary_type_comparison(other, op, Type::object(), range, visitor) - } - } - } - State::UnsupportedOnAllElements(error) => Err(error), - } - } - - /// Infers the type of a binary comparison (e.g. 'left == right'). See - /// `infer_compare_expression` for the higher level logic dealing with multi-comparison - /// expressions. - /// - /// If the operation is not supported, return None (we need upstream context to emit a - /// diagnostic). - fn infer_binary_type_comparison( - &mut self, - left: Type<'db>, - op: ast::CmpOp, - right: Type<'db>, - range: TextRange, - visitor: &BinaryComparisonVisitor<'db>, - ) -> Result, UnsupportedComparisonError<'db>> { - // Note: identity (is, is not) for equal builtin types is unreliable and not part of the - // language spec. - // - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal - // - `[ast::CompOp::IsNot]`: return `true` if unequal, `bool` if equal - let db = self.db(); - let try_dunder = |inference: &mut Self, policy: MemberLookupPolicy| { - let rich_comparison = |op| inference.infer_rich_comparison(left, right, op, policy); - let membership_test_comparison = |op, range: TextRange| { - inference.infer_membership_test_comparison(left, right, op, range) - }; - - match op { - ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In => { - membership_test_comparison(MembershipTestCompareOperator::In, range) - } - ast::CmpOp::NotIn => { - membership_test_comparison(MembershipTestCompareOperator::NotIn, range) - } - ast::CmpOp::Is => { - if left.is_disjoint_from(db, right) { - Ok(Type::bool_literal(false)) - } else if left.is_singleton(db) && left.is_equivalent_to(db, right) { - Ok(Type::bool_literal(true)) - } else { - Ok(KnownClass::Bool.to_instance(db)) - } - } - ast::CmpOp::IsNot => { - if left.is_disjoint_from(db, right) { - Ok(Type::bool_literal(true)) - } else if left.is_singleton(db) && left.is_equivalent_to(db, right) { - Ok(Type::bool_literal(false)) - } else { - Ok(KnownClass::Bool.to_instance(db)) - } - } - } - }; - - let comparison_result = match (left, right) { - (Type::Union(union), other) => { - let mut builder = UnionBuilder::new(self.db()); - for element in union.elements(self.db()) { - builder = - builder.add(self.infer_binary_type_comparison(*element, op, other, range, visitor)?); - } - Some(Ok(builder.build())) - } - (other, Type::Union(union)) => { - let mut builder = UnionBuilder::new(self.db()); - for element in union.elements(self.db()) { - builder = - builder.add(self.infer_binary_type_comparison(other, op, *element, range, visitor)?); - } - Some(Ok(builder.build())) - } - - (Type::Intersection(intersection), right) => { - Some(self.infer_binary_intersection_type_comparison( - intersection, - op, - right, - IntersectionOn::Left, - range, - visitor, - ).map_err(|err|UnsupportedComparisonError { op, left_ty: left, right_ty: err.right_ty })) - } - (left, Type::Intersection(intersection)) => { - Some(self.infer_binary_intersection_type_comparison( - intersection, - op, - left, - IntersectionOn::Right, - range, - visitor, - ).map_err(|err|UnsupportedComparisonError { op, left_ty: err.left_ty, right_ty: right })) - } - - (Type::TypeAlias(alias), right) => Some( - visitor.visit((left, op, right), || { self.infer_binary_type_comparison( - alias.value_type(self.db()), - op, - right, - range, - visitor, - ) - })), - - (left, Type::TypeAlias(alias)) => Some( - visitor.visit((left, op, right), || { self.infer_binary_type_comparison( - left, - op, - alias.value_type(self.db()), + let ty = comparisons::infer_binary_type_comparison( + &builder.context, + left_ty, + *op, + right_ty, range, - visitor, + &BinaryComparisonVisitor::new(Ok(Type::bool_literal(true))), ) - })), - - // `try_dunder` works for almost all `NewType`s, but not for `NewType`s of `float` and - // `complex`, where the concrete base type is a union. In that case it turns out the - // `self` types of the dunder methods in typeshed don't match, because they don't get - // the same `int | float` and `int | float | complex` special treatment that the - // positional arguments get. In those cases we need to explicitly delegate to the base - // type, so that it hits the `Type::Union` branches above. - (Type::NewTypeInstance(newtype), right) => Some( - try_dunder(self, MemberLookupPolicy::default()).or_else(|_| { - visitor.visit((left, op, right), || { - self.infer_binary_type_comparison( - newtype.concrete_base_type(self.db()), - op, - right, - range, - visitor, - ) - }) - }), - ), - (left, Type::NewTypeInstance(newtype)) => Some( - try_dunder(self, MemberLookupPolicy::default()).or_else(|_| { - visitor.visit((left, op, right), || { - self.infer_binary_type_comparison( - left, - op, - newtype.concrete_base_type(self.db()), - range, - visitor, - ) - }) - }), - ), - - // Similar to `NewType`s, `TypeVar`s with union bounds (like `bound=float` which becomes - // `int | float`) need to delegate to the bound type. - // - // When both operands are the same bounded TypeVar, we check the comparison on the bound - // type paired with itself. - (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar)) - if left_tvar.identity(self.db()) == right_tvar.identity(self.db()) => - { - match left_tvar.typevar(self.db()).bound_or_constraints(self.db()) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => Some( - try_dunder(self, MemberLookupPolicy::default()).or_else(|_| { - visitor.visit((left, op, right), || { - self.infer_binary_type_comparison(bound, op, bound, range, visitor) - }) - }), - ), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - // For constrained TypeVars, check each constraint paired with itself. - let mut builder = UnionBuilder::new(self.db()); - for &constraint in constraints.elements(self.db()) { - builder = builder.add(self.infer_binary_type_comparison( - constraint, - op, - constraint, - range, - visitor, - )?); - } - Some(Ok(builder.build())) - } - None => None, // Fall through to default handling - } - } - // When the left operand is a bounded TypeVar and the right is not a TypeVar, - // delegate to the bound type. - (Type::TypeVar(left_tvar), right) if !right.is_type_var() => { - match left_tvar.typevar(self.db()).bound_or_constraints(self.db()) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => Some( - try_dunder(self, MemberLookupPolicy::default()).or_else(|_| { - visitor.visit((left, op, right), || { - self.infer_binary_type_comparison(bound, op, right, range, visitor) - }) - }), - ), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut builder = UnionBuilder::new(self.db()); - for &constraint in constraints.elements(self.db()) { - builder = builder.add(self.infer_binary_type_comparison( - constraint, op, right, range, visitor, - )?); - } - Some(Ok(builder.build())) - } - None => None, - } - } - // When the right operand is a bounded TypeVar and the left is not a TypeVar, - // delegate to the bound type. - (left, Type::TypeVar(right_tvar)) if !left.is_type_var() => { - match right_tvar.typevar(self.db()).bound_or_constraints(self.db()) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => Some( - try_dunder(self, MemberLookupPolicy::default()).or_else(|_| { - visitor.visit((left, op, right), || { - self.infer_binary_type_comparison(left, op, bound, range, visitor) - }) - }), - ), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut builder = UnionBuilder::new(self.db()); - for &constraint in constraints.elements(self.db()) { - builder = builder.add(self.infer_binary_type_comparison( - left, op, constraint, range, visitor, - )?); - } - Some(Ok(builder.build())) - } - None => None, - } - } - - (Type::LiteralValue(left_literal), Type::LiteralValue(right_literal)) => match (left_literal.kind(), right_literal.kind()) { - (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m)) => Some(match op { - ast::CmpOp::Eq => Ok(Type::bool_literal(n == m)), - ast::CmpOp::NotEq => Ok(Type::bool_literal(n != m)), - ast::CmpOp::Lt => Ok(Type::bool_literal(n < m)), - ast::CmpOp::LtE => Ok(Type::bool_literal(n <= m)), - ast::CmpOp::Gt => Ok(Type::bool_literal(n > m)), - ast::CmpOp::GtE => Ok(Type::bool_literal(n >= m)), - // We cannot say that two equal int Literals will return True from an `is` or `is not` comparison. - // Even if they are the same value, they may not be the same object. - ast::CmpOp::Is => { - if n == m { - Ok(KnownClass::Bool.to_instance(self.db())) - } else { - Ok(Type::bool_literal(false)) - } - } - ast::CmpOp::IsNot => { - if n == m { - Ok(KnownClass::Bool.to_instance(self.db())) - } else { - Ok(Type::bool_literal(true)) - } - } - // Undefined for (int, int) - ast::CmpOp::In | ast::CmpOp::NotIn => Err(UnsupportedComparisonError { - op, - left_ty: left, - right_ty: right, - }), - }), - // Booleans are coded as integers (False = 0, True = 1) - (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Bool(b)) => { - Some(self.infer_binary_type_comparison( - Type::int_literal(n.as_i64()), - op, - Type::int_literal(i64::from(b)), - range, - visitor, - ).map_err(|_|UnsupportedComparisonError {op, left_ty: left, right_ty: right})) - } - (LiteralValueTypeKind::Bool(b), LiteralValueTypeKind::Int(m)) => { - Some(self.infer_binary_type_comparison( - Type::int_literal(i64::from(b)), - op, - Type::int_literal(m.as_i64()), - range, - visitor, - ).map_err(|_|UnsupportedComparisonError {op, left_ty: left, right_ty: right})) - } - (LiteralValueTypeKind::Bool(a), LiteralValueTypeKind::Bool(b)) => { - Some(self.infer_binary_type_comparison( - Type::int_literal(i64::from(a)), - op, - Type::int_literal(i64::from(b)), + .unwrap_or_else(|error| { + report_unsupported_comparison( + &builder.context, + &error, range, - visitor, - ).map_err(|_|UnsupportedComparisonError {op, left_ty: left, right_ty: right})) - } - - (LiteralValueTypeKind::String(salsa_s1), LiteralValueTypeKind::String(salsa_s2)) => { - let s1 = salsa_s1.value(self.db()); - let s2 = salsa_s2.value(self.db()); - let result = match op { - ast::CmpOp::Eq => Type::bool_literal(s1 == s2), - ast::CmpOp::NotEq => Type::bool_literal(s1 != s2), - ast::CmpOp::Lt => Type::bool_literal(s1 < s2), - ast::CmpOp::LtE => Type::bool_literal(s1 <= s2), - ast::CmpOp::Gt => Type::bool_literal(s1 > s2), - ast::CmpOp::GtE => Type::bool_literal(s1 >= s2), - ast::CmpOp::In => Type::bool_literal(s2.contains(s1)), - ast::CmpOp::NotIn => Type::bool_literal(!s2.contains(s1)), - ast::CmpOp::Is => { - if s1 == s2 { - KnownClass::Bool.to_instance(self.db()) - } else { - Type::bool_literal(false) - } - } - ast::CmpOp::IsNot => { - if s1 == s2 { - KnownClass::Bool.to_instance(self.db()) - } else { - Type::bool_literal(true) - } - } - }; - Some(Ok(result)) - } - - (LiteralValueTypeKind::Bytes(salsa_b1), LiteralValueTypeKind::Bytes(salsa_b2)) => { - let b1 = salsa_b1.value(self.db()); - let b2 = salsa_b2.value(self.db()); - let result = match op { - ast::CmpOp::Eq => Type::bool_literal(b1 == b2), - ast::CmpOp::NotEq => Type::bool_literal(b1 != b2), - ast::CmpOp::Lt => Type::bool_literal(b1 < b2), - ast::CmpOp::LtE => Type::bool_literal(b1 <= b2), - ast::CmpOp::Gt => Type::bool_literal(b1 > b2), - ast::CmpOp::GtE => Type::bool_literal(b1 >= b2), - ast::CmpOp::In => { - Type::bool_literal(memchr::memmem::find(b2, b1).is_some()) - } - ast::CmpOp::NotIn => { - Type::bool_literal(memchr::memmem::find(b2, b1).is_none()) - } - ast::CmpOp::Is => { - if b1 == b2 { - KnownClass::Bool.to_instance(self.db()) - } else { - Type::bool_literal(false) - } - } - ast::CmpOp::IsNot => { - if b1 == b2 { - KnownClass::Bool.to_instance(self.db()) - } else { - Type::bool_literal(true) - } - } - }; - Some(Ok(result)) - } - - (LiteralValueTypeKind::Enum(literal_1), LiteralValueTypeKind::Enum(literal_2)) - if op == ast::CmpOp::Eq => - { - Some(Ok(match try_dunder(self, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) { - Ok(ty) => ty, - Err(_) => Type::bool_literal(literal_1 == literal_2), - })) - } - (LiteralValueTypeKind::Enum(literal_1), LiteralValueTypeKind::Enum(literal_2)) - if op == ast::CmpOp::NotEq => - { - Some(Ok(match try_dunder(self, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) { - Ok(ty) => ty, - Err(_) => Type::bool_literal(literal_1 != literal_2), - })) - } - - _ => None - } - - ( - Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), - Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), - ) => { - let constraints = ConstraintSetBuilder::new(); - let left = constraints.load(left.constraints(self.db())); - let right = constraints.load(right.constraints(self.db())); - let result = left.iff(self.db(), &constraints, right); - let equivalent = result.is_always_satisfied(self.db()); - match op { - ast::CmpOp::Eq => Some(Ok(Type::bool_literal(equivalent))), - ast::CmpOp::NotEq => Some(Ok(Type::bool_literal(!equivalent))), - _ => None, - } - } - - ( - Type::NominalInstance(nominal1), - Type::NominalInstance(nominal2), - ) => nominal1.tuple_spec(self.db()) - .and_then(|lhs_tuple| Some((lhs_tuple, nominal2.tuple_spec(self.db())?))) - .map(|(lhs_tuple, rhs_tuple)| { - let mut tuple_rich_comparison = - |rich_op| visitor.visit((left, op, right), || { - self.infer_tuple_rich_comparison(&lhs_tuple, rich_op, &rhs_tuple, range, visitor) - }); + left, + right, + left_ty, + right_ty, + ); match op { - ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In | ast::CmpOp::NotIn => { - let mut any_eq = false; - let mut any_ambiguous = false; - - for ty in rhs_tuple.iter_all_elements() { - let eq_result = self.infer_binary_type_comparison( - left, - ast::CmpOp::Eq, - ty, - range, - visitor - ).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); - - match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for different union variants. Instead, this is just for us to - // evaluate a possibly truthy value to `false` or `true`. - ty => match ty.bool(self.db()) { - Truthiness::AlwaysTrue => any_eq = true, - Truthiness::AlwaysFalse => (), - Truthiness::Ambiguous => any_ambiguous = true, - }, - } - } - - if any_eq { - Ok(Type::bool_literal(op.is_in())) - } else if !any_ambiguous { - Ok(Type::bool_literal(op.is_not_in())) - } else { - Ok(KnownClass::Bool.to_instance(self.db())) - } - } - ast::CmpOp::Is | ast::CmpOp::IsNot => { - // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` - // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` - let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect( - "infer_binary_type_comparison should never return None for `CmpOp::Eq`", - ); - - Ok(match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for `is` and `is not` comparisons. This is an implementation detail - // for how we determine the truthiness of a type. - ty => match ty.bool(self.db()) { - Truthiness::AlwaysFalse => Type::bool_literal(op.is_is_not()), - _ => KnownClass::Bool.to_instance(self.db()), - }, - }) + // `in, not in, is, is not` always return bool instances + ast::CmpOp::In | ast::CmpOp::NotIn | ast::CmpOp::Is | ast::CmpOp::IsNot => { + KnownClass::Bool.to_instance(builder.db()) } + // Other operators can return arbitrary types + _ => Type::unknown(), } - } - ), - - _ => None, - }; - - if let Some(result) = comparison_result { - return result; - } - - // Final generalized fallback: lookup the rich comparison `__dunder__` methods - try_dunder(self, MemberLookupPolicy::default()) - } - - /// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their - /// behaviour can be edited for classes by implementing corresponding dunder methods. - /// This function performs rich comparison between two types and returns the resulting type. - /// see `` - fn infer_rich_comparison( - &self, - left: Type<'db>, - right: Type<'db>, - op: RichCompareOperator, - policy: MemberLookupPolicy, - ) -> Result, UnsupportedComparisonError<'db>> { - let db = self.db(); - // The following resource has details about the rich comparison algorithm: - // https://snarky.ca/unravelling-rich-comparison-operators/ - let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| { - left.try_call_dunder_with_policy( - db, - op.dunder(), - &mut CallArguments::positional([right]), - TypeContext::default(), - policy, - ) - .map(|outcome| outcome.return_type(db)) - .ok() - }; - - // The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side. - if left != right && right.is_subtype_of(db, left) { - call_dunder(op.reflect(), right, left).or_else(|| call_dunder(op, left, right)) - } else { - call_dunder(op, left, right).or_else(|| call_dunder(op.reflect(), right, left)) - } - .or_else(|| { - // When no appropriate method returns any value other than NotImplemented, - // the `==` and `!=` operators will fall back to `is` and `is not`, respectively. - // refer to `` - if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) - // This branch implements specific behavior of the `__eq__` and `__ne__` methods - // on `object`, so it does not apply if we skip looking up attributes on `object`. - && !policy.mro_no_object_fallback() - { - Some(KnownClass::Bool.to_instance(db)) - } else { - None - } - }) - .ok_or_else(|| UnsupportedComparisonError { - op: op.into(), - left_ty: left, - right_ty: right, - }) - } - - /// Performs a membership test (`in` and `not in`) between two instances and returns the resulting type, or `None` if the test is unsupported. - /// The behavior can be customized in Python by implementing `__contains__`, `__iter__`, or `__getitem__` methods. - /// See `` - /// and `` - fn infer_membership_test_comparison( - &self, - left: Type<'db>, - right: Type<'db>, - op: MembershipTestCompareOperator, - range: TextRange, - ) -> Result, UnsupportedComparisonError<'db>> { - let db = self.db(); - - let compare_result_opt = match right.try_call_dunder( - db, - "__contains__", - CallArguments::positional([left]), - TypeContext::default(), - ) { - // If `__contains__` is available, it is used directly for the membership test. - Ok(bindings) => Some(bindings.return_type(db)), - // If `__contains__` is not available or possibly unbound, - // fall back to iteration-based membership test. - Err(CallDunderError::MethodNotAvailable | CallDunderError::PossiblyUnbound(_)) => right - .try_iterate(db) - .map(|_| KnownClass::Bool.to_instance(db)) - .ok(), - // `__contains__` exists but can't be called with the given arguments. - Err(CallDunderError::CallError(..)) => None, - }; - - compare_result_opt - .map(|ty| { - if matches!(ty, Type::Dynamic(DynamicType::Todo(_))) { - return ty; - } - - let truthiness = ty.try_bool(db).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, range); - err.fallback_truthiness() }); - match op { - MembershipTestCompareOperator::In => truthiness.into_type(db), - MembershipTestCompareOperator::NotIn => truthiness.negate().into_type(db), - } - }) - .ok_or_else(|| UnsupportedComparisonError { - op: op.into(), - left_ty: left, - right_ty: right, - }) - } - - /// Simulates rich comparison between tuples and returns the inferred result. - /// This performs a lexicographic comparison, returning a union of all possible return types that could result from the comparison. - /// - /// basically it's based on cpython's `tuple_richcompare` - /// see `` - fn infer_tuple_rich_comparison( - &mut self, - left: &TupleSpec<'db>, - op: RichCompareOperator, - right: &TupleSpec<'db>, - range: TextRange, - visitor: &BinaryComparisonVisitor<'db>, - ) -> Result, UnsupportedComparisonError<'db>> { - match (left, right) { - // Both fixed-length: perform full lexicographic comparison. - (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) => { - let left_iter = left.iter_all_elements(); - let right_iter = right.iter_all_elements(); - - let mut builder = UnionBuilder::new(self.db()); - - for (l_ty, r_ty) in left_iter.zip(right_iter) { - let pairwise_eq_result = self - .infer_binary_type_comparison(l_ty, ast::CmpOp::Eq, r_ty, range, visitor) - .expect( - "infer_binary_type_comparison should never return None for `CmpOp::Eq`", - ); - - match pairwise_eq_result - .try_bool(self.db()) - .unwrap_or_else(|err| { - // TODO: We should, whenever possible, pass the range of the left and right elements - // instead of the range of the whole tuple. - err.report_diagnostic(&self.context, range); - err.fallback_truthiness() - }) { - // - AlwaysTrue : Continue to the next pair for lexicographic comparison - Truthiness::AlwaysTrue => continue, - // - AlwaysFalse: - // Lexicographic comparisons will always terminate with this pair. - // Complete the comparison and return the result. - // - Ambiguous: - // Lexicographic comparisons might continue to the next pair (if eq_result is true), - // or terminate here (if eq_result is false). - // To account for cases where the comparison terminates here, add the pairwise comparison result to the union builder. - eq_truthiness @ (Truthiness::AlwaysFalse | Truthiness::Ambiguous) => { - let pairwise_compare_result = match op { - RichCompareOperator::Lt - | RichCompareOperator::Le - | RichCompareOperator::Gt - | RichCompareOperator::Ge => self.infer_binary_type_comparison( - l_ty, - op.into(), - r_ty, - range, - visitor, - )?, - // For `==` and `!=`, we already figure out the result from `pairwise_eq_result` - // NOTE: The CPython implementation does not account for non-boolean return types - // or cases where `!=` is not the negation of `==`, we also do not consider these cases. - RichCompareOperator::Eq => Type::bool_literal(false), - RichCompareOperator::Ne => Type::bool_literal(true), - }; - - builder = builder.add(pairwise_compare_result); - - if eq_truthiness.is_ambiguous() { - continue; - } - - return Ok(builder.build()); - } - } - } - - // if no more items to compare, we just compare sizes - let (left_len, right_len) = (left.len(), right.len()); - - builder = builder.add(Type::bool_literal(match op { - RichCompareOperator::Eq => left_len == right_len, - RichCompareOperator::Ne => left_len != right_len, - RichCompareOperator::Lt => left_len < right_len, - RichCompareOperator::Le => left_len <= right_len, - RichCompareOperator::Gt => left_len > right_len, - RichCompareOperator::Ge => left_len >= right_len, - })); - - Ok(builder.build()) - } - - // At least one tuple is variable-length. We can make no assumptions about - // the relative lengths of the tuples, and therefore neither about how they - // compare lexicographically. However, we still need to verify that the - // element types are comparable for ordering comparisons. - - // For equality comparisons (==, !=), any two objects can be compared, - // and tuple equality always returns bool regardless of element __eq__ return types. - (TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) - if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) => - { - Ok(KnownClass::Bool.to_instance(self.db())) - } - - // At least one variable-length: check all elements that could potentially be compared. - // We use `try_for_each_element_pair` to iterate over all possible pairings. - (left @ TupleSpec::Variable(_), right) | (left, right @ TupleSpec::Variable(_)) => { - let mut results = smallvec::SmallVec::<[Type<'db>; 8]>::new(); - left.try_for_each_element_pair(right, |l_ty, r_ty| { - results.push(self.infer_binary_type_comparison( - l_ty, - op.into(), - r_ty, - range, - visitor, - )?); - Ok::<_, UnsupportedComparisonError<'db>>(()) - })?; - - let mut builder = UnionBuilder::new(self.db()); - for result in results { - builder = builder.add(result); - } - // Length comparison (when all elements are equal) returns bool. - builder = builder.add(KnownClass::Bool.to_instance(self.db())); - - Ok(builder.build()) - } - } + (ty, range) + }, + ) } fn infer_subscript_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { @@ -17631,88 +16742,6 @@ impl From for DeferredExpressionState { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RichCompareOperator { - Eq, - Ne, - Gt, - Ge, - Lt, - Le, -} - -impl From for ast::CmpOp { - fn from(value: RichCompareOperator) -> Self { - match value { - RichCompareOperator::Eq => ast::CmpOp::Eq, - RichCompareOperator::Ne => ast::CmpOp::NotEq, - RichCompareOperator::Lt => ast::CmpOp::Lt, - RichCompareOperator::Le => ast::CmpOp::LtE, - RichCompareOperator::Gt => ast::CmpOp::Gt, - RichCompareOperator::Ge => ast::CmpOp::GtE, - } - } -} - -impl RichCompareOperator { - #[must_use] - const fn dunder(self) -> &'static str { - match self { - RichCompareOperator::Eq => "__eq__", - RichCompareOperator::Ne => "__ne__", - RichCompareOperator::Lt => "__lt__", - RichCompareOperator::Le => "__le__", - RichCompareOperator::Gt => "__gt__", - RichCompareOperator::Ge => "__ge__", - } - } - - #[must_use] - const fn reflect(self) -> Self { - match self { - RichCompareOperator::Eq => RichCompareOperator::Eq, - RichCompareOperator::Ne => RichCompareOperator::Ne, - RichCompareOperator::Lt => RichCompareOperator::Gt, - RichCompareOperator::Le => RichCompareOperator::Ge, - RichCompareOperator::Gt => RichCompareOperator::Lt, - RichCompareOperator::Ge => RichCompareOperator::Le, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MembershipTestCompareOperator { - In, - NotIn, -} - -impl From for ast::CmpOp { - fn from(value: MembershipTestCompareOperator) -> Self { - match value { - MembershipTestCompareOperator::In => ast::CmpOp::In, - MembershipTestCompareOperator::NotIn => ast::CmpOp::NotIn, - } - } -} - -/// Context for a failed comparison operation. -/// -/// `left_ty` and `right_ty` are the "low-level" types -/// that cannot be compared using `op`. For example, -/// when evaluating `(1, "foo") < (2, 3)`, the "high-level" -/// types of the operands are `tuple[Literal[1], Literal["foo"]]` -/// and `tuple[Literal[2], Literal[3]]`. Those aren't captured -/// in this struct, but the "low-level" types that mean that -/// the high-level types cannot be compared *are* captured in -/// this struct. In this case, those would be `Literal["foo"]` -/// and `Literal[3]`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct UnsupportedComparisonError<'db> { - pub(crate) op: ast::CmpOp, - pub(crate) left_ty: Type<'db>, - pub(crate) right_ty: Type<'db>, -} - fn format_import_from_module(level: u32, module: Option<&str>) -> String { format!( "{}{}", diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs new file mode 100644 index 0000000000000..a1f3b109efdcd --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -0,0 +1,1035 @@ +use ruff_python_ast as ast; +use ruff_text_size::TextRange; +use smallvec::SmallVec; + +use crate::Db; +use crate::types::call::{CallArguments, CallDunderError}; +use crate::types::constraints::ConstraintSetBuilder; +use crate::types::context::InferContext; +use crate::types::cyclic::CycleDetector; +use crate::types::tuple::TupleSpec; +use crate::types::{ + DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, + LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Truthiness, Type, TypeContext, + TypeVarBoundOrConstraints, UnionBuilder, +}; + +/// Whether the intersection type is on the left or right side of the comparison. +#[derive(Debug, Clone, Copy)] +enum IntersectionOn { + Left, + Right, +} + +/// A [`CycleDetector`] that is used in [`infer_binary_type_comparison`]. +pub(super) type BinaryComparisonVisitor<'db> = CycleDetector< + ast::CmpOp, + (Type<'db>, ast::CmpOp, Type<'db>), + Result, UnsupportedComparisonError<'db>>, +>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RichCompareOperator { + Eq, + Ne, + Gt, + Ge, + Lt, + Le, +} + +impl From for ast::CmpOp { + fn from(value: RichCompareOperator) -> Self { + match value { + RichCompareOperator::Eq => ast::CmpOp::Eq, + RichCompareOperator::Ne => ast::CmpOp::NotEq, + RichCompareOperator::Lt => ast::CmpOp::Lt, + RichCompareOperator::Le => ast::CmpOp::LtE, + RichCompareOperator::Gt => ast::CmpOp::Gt, + RichCompareOperator::Ge => ast::CmpOp::GtE, + } + } +} + +impl RichCompareOperator { + #[must_use] + const fn dunder(self) -> &'static str { + match self { + RichCompareOperator::Eq => "__eq__", + RichCompareOperator::Ne => "__ne__", + RichCompareOperator::Lt => "__lt__", + RichCompareOperator::Le => "__le__", + RichCompareOperator::Gt => "__gt__", + RichCompareOperator::Ge => "__ge__", + } + } + + #[must_use] + const fn reflect(self) -> Self { + match self { + RichCompareOperator::Eq => RichCompareOperator::Eq, + RichCompareOperator::Ne => RichCompareOperator::Ne, + RichCompareOperator::Lt => RichCompareOperator::Gt, + RichCompareOperator::Le => RichCompareOperator::Ge, + RichCompareOperator::Gt => RichCompareOperator::Lt, + RichCompareOperator::Ge => RichCompareOperator::Le, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MembershipTestCompareOperator { + In, + NotIn, +} + +impl From for ast::CmpOp { + fn from(value: MembershipTestCompareOperator) -> Self { + match value { + MembershipTestCompareOperator::In => ast::CmpOp::In, + MembershipTestCompareOperator::NotIn => ast::CmpOp::NotIn, + } + } +} + +/// Context for a failed comparison operation. +/// +/// `left_ty` and `right_ty` are the "low-level" types +/// that cannot be compared using `op`. For example, +/// when evaluating `(1, "foo") < (2, 3)`, the "high-level" +/// types of the operands are `tuple[Literal[1], Literal["foo"]]` +/// and `tuple[Literal[2], Literal[3]]`. Those aren't captured +/// in this struct, but the "low-level" types that mean that +/// the high-level types cannot be compared *are* captured in +/// this struct. In this case, those would be `Literal["foo"]` +/// and `Literal[3]`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct UnsupportedComparisonError<'db> { + pub(crate) op: ast::CmpOp, + pub(crate) left_ty: Type<'db>, + pub(crate) right_ty: Type<'db>, +} + +/// Infers the type of a binary comparison (e.g. 'left == right'). See +/// `TypeInferenceBuilder::infer_compare_expression` for the higher level logic dealing with +/// multi-comparison expressions. +/// +/// If the operation is not supported, return an error (we need upstream context to emit a +/// diagnostic). +pub(super) fn infer_binary_type_comparison<'db>( + context: &InferContext<'db, '_>, + left: Type<'db>, + op: ast::CmpOp, + right: Type<'db>, + range: TextRange, + visitor: &BinaryComparisonVisitor<'db>, +) -> Result, UnsupportedComparisonError<'db>> { + let db = context.db(); + + // Note: identity (is, is not) for equal builtin types is unreliable and not part of the + // language spec. + // - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal + // - `[ast::CompOp::IsNot]`: return `true` if unequal, `bool` if equal + let try_dunder = |policy: MemberLookupPolicy| { + let rich_comparison = |op| infer_rich_comparison(db, left, right, op, policy); + let membership_test_comparison = |op, range: TextRange| { + infer_membership_test_comparison(context, left, right, op, range) + }; + + match op { + ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq), + ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne), + ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt), + ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le), + ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt), + ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge), + ast::CmpOp::In => membership_test_comparison(MembershipTestCompareOperator::In, range), + ast::CmpOp::NotIn => { + membership_test_comparison(MembershipTestCompareOperator::NotIn, range) + } + ast::CmpOp::Is => { + if left.is_disjoint_from(db, right) { + Ok(Type::bool_literal(false)) + } else if left.is_singleton(db) && left.is_equivalent_to(db, right) { + Ok(Type::bool_literal(true)) + } else { + Ok(KnownClass::Bool.to_instance(db)) + } + } + ast::CmpOp::IsNot => { + if left.is_disjoint_from(db, right) { + Ok(Type::bool_literal(true)) + } else if left.is_singleton(db) && left.is_equivalent_to(db, right) { + Ok(Type::bool_literal(false)) + } else { + Ok(KnownClass::Bool.to_instance(db)) + } + } + } + }; + + let comparison_result = match (left, right) { + (Type::Union(union), other) => { + let mut builder = UnionBuilder::new(db); + for element in union.elements(db) { + builder = builder.add(infer_binary_type_comparison( + context, *element, op, other, range, visitor, + )?); + } + Some(Ok(builder.build())) + } + (other, Type::Union(union)) => { + let mut builder = UnionBuilder::new(db); + for element in union.elements(db) { + builder = builder.add(infer_binary_type_comparison( + context, other, op, *element, range, visitor, + )?); + } + Some(Ok(builder.build())) + } + + (Type::Intersection(intersection), right) => { + Some( + infer_binary_intersection_type_comparison( + context, + intersection, + op, + right, + IntersectionOn::Left, + range, + visitor, + ) + .map_err(|err| UnsupportedComparisonError { + op, + left_ty: left, + right_ty: err.right_ty, + }), + ) + } + (left, Type::Intersection(intersection)) => { + Some( + infer_binary_intersection_type_comparison( + context, + intersection, + op, + left, + IntersectionOn::Right, + range, + visitor, + ) + .map_err(|err| UnsupportedComparisonError { + op, + left_ty: err.left_ty, + right_ty: right, + }), + ) + } + + (Type::TypeAlias(alias), right) => Some(visitor.visit((left, op, right), || { + infer_binary_type_comparison(context, alias.value_type(db), op, right, range, visitor) + })), + + (left, Type::TypeAlias(alias)) => Some(visitor.visit((left, op, right), || { + infer_binary_type_comparison(context, left, op, alias.value_type(db), range, visitor) + })), + + // `try_dunder` works for almost all `NewType`s, but not for `NewType`s of `float` and + // `complex`, where the concrete base type is a union. In that case it turns out the + // `self` types of the dunder methods in typeshed don't match, because they don't get + // the same `int | float` and `int | float | complex` special treatment that the + // positional arguments get. In those cases we need to explicitly delegate to the base + // type, so that it hits the `Type::Union` branches above. + (Type::NewTypeInstance(newtype), right) => Some( + try_dunder(MemberLookupPolicy::default()).or_else(|_| { + visitor.visit((left, op, right), || { + infer_binary_type_comparison( + context, + newtype.concrete_base_type(db), + op, + right, + range, + visitor, + ) + }) + }), + ), + (left, Type::NewTypeInstance(newtype)) => Some( + try_dunder(MemberLookupPolicy::default()).or_else(|_| { + visitor.visit((left, op, right), || { + infer_binary_type_comparison( + context, + left, + op, + newtype.concrete_base_type(db), + range, + visitor, + ) + }) + }), + ), + + // Similar to `NewType`s, `TypeVar`s with union bounds (like `bound=float` which becomes + // `int | float`) need to delegate to the bound type. + // + // When both operands are the same bounded TypeVar, we check the comparison on the bound + // type paired with itself. + (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar)) + if left_tvar.identity(db) == right_tvar.identity(db) => + { + match left_tvar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => Some( + try_dunder(MemberLookupPolicy::default()).or_else(|_| { + visitor.visit((left, op, right), || { + infer_binary_type_comparison( + context, bound, op, bound, range, visitor, + ) + }) + }), + ), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + // For constrained TypeVars, check each constraint paired with itself. + let mut builder = UnionBuilder::new(db); + for &constraint in constraints.elements(db) { + builder = builder.add(infer_binary_type_comparison( + context, constraint, op, constraint, range, visitor, + )?); + } + Some(Ok(builder.build())) + } + None => None, // Fall through to default handling + } + } + // When the left operand is a bounded TypeVar and the right is not a TypeVar, + // delegate to the bound type. + (Type::TypeVar(left_tvar), right) if !right.is_type_var() => { + match left_tvar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => Some( + try_dunder(MemberLookupPolicy::default()).or_else(|_| { + visitor.visit((left, op, right), || { + infer_binary_type_comparison( + context, bound, op, right, range, visitor, + ) + }) + }), + ), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + let mut builder = UnionBuilder::new(db); + for &constraint in constraints.elements(db) { + builder = builder.add(infer_binary_type_comparison( + context, constraint, op, right, range, visitor, + )?); + } + Some(Ok(builder.build())) + } + None => None, + } + } + // When the right operand is a bounded TypeVar and the left is not a TypeVar, + // delegate to the bound type. + (left, Type::TypeVar(right_tvar)) if !left.is_type_var() => { + match right_tvar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => Some( + try_dunder(MemberLookupPolicy::default()).or_else(|_| { + visitor.visit((left, op, right), || { + infer_binary_type_comparison( + context, left, op, bound, range, visitor, + ) + }) + }), + ), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + let mut builder = UnionBuilder::new(db); + for &constraint in constraints.elements(db) { + builder = builder.add(infer_binary_type_comparison( + context, left, op, constraint, range, visitor, + )?); + } + Some(Ok(builder.build())) + } + None => None, + } + } + + (Type::LiteralValue(left_literal), Type::LiteralValue(right_literal)) => { + match (left_literal.kind(), right_literal.kind()) { + (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m)) => { + Some(match op { + ast::CmpOp::Eq => Ok(Type::bool_literal(n == m)), + ast::CmpOp::NotEq => Ok(Type::bool_literal(n != m)), + ast::CmpOp::Lt => Ok(Type::bool_literal(n < m)), + ast::CmpOp::LtE => Ok(Type::bool_literal(n <= m)), + ast::CmpOp::Gt => Ok(Type::bool_literal(n > m)), + ast::CmpOp::GtE => Ok(Type::bool_literal(n >= m)), + // We cannot say that two equal int Literals will return True from an `is` or `is not` comparison. + // Even if they are the same value, they may not be the same object. + ast::CmpOp::Is => { + if n == m { + Ok(KnownClass::Bool.to_instance(db)) + } else { + Ok(Type::bool_literal(false)) + } + } + ast::CmpOp::IsNot => { + if n == m { + Ok(KnownClass::Bool.to_instance(db)) + } else { + Ok(Type::bool_literal(true)) + } + } + // Undefined for (int, int) + ast::CmpOp::In | ast::CmpOp::NotIn => Err(UnsupportedComparisonError { + op, + left_ty: left, + right_ty: right, + }), + }) + } + // Booleans are coded as integers (False = 0, True = 1) + (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Bool(b)) => Some( + infer_binary_type_comparison( + context, + Type::int_literal(n.as_i64()), + op, + Type::int_literal(i64::from(b)), + range, + visitor, + ) + .map_err(|_| UnsupportedComparisonError { + op, + left_ty: left, + right_ty: right, + }), + ), + (LiteralValueTypeKind::Bool(b), LiteralValueTypeKind::Int(m)) => Some( + infer_binary_type_comparison( + context, + Type::int_literal(i64::from(b)), + op, + Type::int_literal(m.as_i64()), + range, + visitor, + ) + .map_err(|_| UnsupportedComparisonError { + op, + left_ty: left, + right_ty: right, + }), + ), + (LiteralValueTypeKind::Bool(a), LiteralValueTypeKind::Bool(b)) => Some( + infer_binary_type_comparison( + context, + Type::int_literal(i64::from(a)), + op, + Type::int_literal(i64::from(b)), + range, + visitor, + ) + .map_err(|_| UnsupportedComparisonError { + op, + left_ty: left, + right_ty: right, + }), + ), + + ( + LiteralValueTypeKind::String(salsa_s1), + LiteralValueTypeKind::String(salsa_s2), + ) => { + let s1 = salsa_s1.value(db); + let s2 = salsa_s2.value(db); + let result = match op { + ast::CmpOp::Eq => Type::bool_literal(s1 == s2), + ast::CmpOp::NotEq => Type::bool_literal(s1 != s2), + ast::CmpOp::Lt => Type::bool_literal(s1 < s2), + ast::CmpOp::LtE => Type::bool_literal(s1 <= s2), + ast::CmpOp::Gt => Type::bool_literal(s1 > s2), + ast::CmpOp::GtE => Type::bool_literal(s1 >= s2), + ast::CmpOp::In => Type::bool_literal(s2.contains(s1)), + ast::CmpOp::NotIn => Type::bool_literal(!s2.contains(s1)), + ast::CmpOp::Is => { + if s1 == s2 { + KnownClass::Bool.to_instance(db) + } else { + Type::bool_literal(false) + } + } + ast::CmpOp::IsNot => { + if s1 == s2 { + KnownClass::Bool.to_instance(db) + } else { + Type::bool_literal(true) + } + } + }; + Some(Ok(result)) + } + + ( + LiteralValueTypeKind::Bytes(salsa_b1), + LiteralValueTypeKind::Bytes(salsa_b2), + ) => { + let b1 = salsa_b1.value(db); + let b2 = salsa_b2.value(db); + let result = match op { + ast::CmpOp::Eq => Type::bool_literal(b1 == b2), + ast::CmpOp::NotEq => Type::bool_literal(b1 != b2), + ast::CmpOp::Lt => Type::bool_literal(b1 < b2), + ast::CmpOp::LtE => Type::bool_literal(b1 <= b2), + ast::CmpOp::Gt => Type::bool_literal(b1 > b2), + ast::CmpOp::GtE => Type::bool_literal(b1 >= b2), + ast::CmpOp::In => { + Type::bool_literal(memchr::memmem::find(b2, b1).is_some()) + } + ast::CmpOp::NotIn => { + Type::bool_literal(memchr::memmem::find(b2, b1).is_none()) + } + ast::CmpOp::Is => { + if b1 == b2 { + KnownClass::Bool.to_instance(db) + } else { + Type::bool_literal(false) + } + } + ast::CmpOp::IsNot => { + if b1 == b2 { + KnownClass::Bool.to_instance(db) + } else { + Type::bool_literal(true) + } + } + }; + Some(Ok(result)) + } + + (LiteralValueTypeKind::Enum(literal_1), LiteralValueTypeKind::Enum(literal_2)) + if op == ast::CmpOp::Eq => + { + Some(Ok( + match try_dunder(MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) { + Ok(ty) => ty, + Err(_) => Type::bool_literal(literal_1 == literal_2), + }, + )) + } + (LiteralValueTypeKind::Enum(literal_1), LiteralValueTypeKind::Enum(literal_2)) + if op == ast::CmpOp::NotEq => + { + Some(Ok( + match try_dunder(MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) { + Ok(ty) => ty, + Err(_) => Type::bool_literal(literal_1 != literal_2), + }, + )) + } + + _ => None, + } + } + + ( + Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), + Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), + ) => { + let constraints = ConstraintSetBuilder::new(); + let left = constraints.load(left.constraints(db)); + let right = constraints.load(right.constraints(db)); + let result = left.iff(db, &constraints, right); + let equivalent = result.is_always_satisfied(db); + match op { + ast::CmpOp::Eq => Some(Ok(Type::bool_literal(equivalent))), + ast::CmpOp::NotEq => Some(Ok(Type::bool_literal(!equivalent))), + _ => None, + } + } + + (Type::NominalInstance(nominal1), Type::NominalInstance(nominal2)) => nominal1 + .tuple_spec(db) + .and_then(|lhs_tuple| Some((lhs_tuple, nominal2.tuple_spec(db)?))) + .map(|(lhs_tuple, rhs_tuple)| { + let tuple_rich_comparison = |rich_op| { + visitor.visit((left, op, right), || { + infer_tuple_rich_comparison( + context, &lhs_tuple, rich_op, &rhs_tuple, range, visitor, + ) + }) + }; + + match op { + ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), + ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), + ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), + ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), + ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), + ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), + ast::CmpOp::In | ast::CmpOp::NotIn => { + let mut any_eq = false; + let mut any_ambiguous = false; + + for ty in rhs_tuple.iter_all_elements() { + let eq_result = infer_binary_type_comparison( + context, + left, + ast::CmpOp::Eq, + ty, + range, + visitor, + ) + .expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); + + match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for different union variants. Instead, this is just for us to + // evaluate a possibly truthy value to `false` or `true`. + ty => match ty.bool(db) { + Truthiness::AlwaysTrue => any_eq = true, + Truthiness::AlwaysFalse => (), + Truthiness::Ambiguous => any_ambiguous = true, + }, + } + } + + if any_eq { + Ok(Type::bool_literal(op.is_in())) + } else if !any_ambiguous { + Ok(Type::bool_literal(op.is_not_in())) + } else { + Ok(KnownClass::Bool.to_instance(db)) + } + } + ast::CmpOp::Is | ast::CmpOp::IsNot => { + // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` + // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` + let eq_result = + tuple_rich_comparison(RichCompareOperator::Eq).expect( + "infer_binary_type_comparison should never return None for `CmpOp::Eq`", + ); + + Ok(match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for `is` and `is not` comparisons. This is an implementation detail + // for how we determine the truthiness of a type. + ty => match ty.bool(db) { + Truthiness::AlwaysFalse => Type::bool_literal(op.is_is_not()), + _ => KnownClass::Bool.to_instance(db), + }, + }) + } + } + }), + + _ => None, + }; + + if let Some(result) = comparison_result { + return result; + } + + // Final generalized fallback: lookup the rich comparison `__dunder__` methods + try_dunder(MemberLookupPolicy::default()) +} + +fn infer_binary_intersection_type_comparison<'db>( + context: &InferContext<'db, '_>, + intersection: IntersectionType<'db>, + op: ast::CmpOp, + other: Type<'db>, + intersection_on: IntersectionOn, + range: TextRange, + visitor: &BinaryComparisonVisitor<'db>, +) -> Result, UnsupportedComparisonError<'db>> { + enum State<'db> { + // We have not seen any positive elements (yet) + NoPositiveElements, + // The operator was unsupported on all elements that we have seen so far. + // Contains the first error we encountered. + UnsupportedOnAllElements(UnsupportedComparisonError<'db>), + // The operator was supported on at least one positive element. + Supported, + } + + let db = context.db(); + + // If a comparison yields a definitive true/false answer on a (positive) part + // of an intersection type, it will also yield a definitive answer on the full + // intersection type, which is even more specific. + for pos in intersection.positive(db) { + let result = match intersection_on { + IntersectionOn::Left => { + infer_binary_type_comparison(context, *pos, op, other, range, visitor) + } + IntersectionOn::Right => { + infer_binary_type_comparison(context, other, op, *pos, range, visitor) + } + }; + + if result + .ok() + .and_then(Type::as_literal_value) + .is_some_and(LiteralValueType::is_bool) + { + return result; + } + } + + // For negative contributions to the intersection type, there are only a few + // special cases that allow us to narrow down the result type of the comparison. + for neg in intersection.negative(db) { + let result = match intersection_on { + IntersectionOn::Left => { + infer_binary_type_comparison(context, *neg, op, other, range, visitor).ok() + } + IntersectionOn::Right => { + infer_binary_type_comparison(context, other, op, *neg, range, visitor).ok() + } + } + .and_then(Type::as_literal_value_kind); + + match (op, result) { + (ast::CmpOp::Is, Some(LiteralValueTypeKind::Bool(true))) => { + return Ok(Type::bool_literal(false)); + } + (ast::CmpOp::IsNot, Some(LiteralValueTypeKind::Bool(false))) => { + return Ok(Type::bool_literal(true)); + } + _ => {} + } + } + + // If none of the simplifications above apply, we still need to return *some* + // result type for the comparison 'T_inter `op` T_other' (or reversed), where + // + // T_inter = P1 & P2 & ... & Pn & ~N1 & ~N2 & ... & ~Nm + // + // is the intersection type. If f(T) is the function that computes the result + // type of a `op`-comparison with `T_other`, we are interested in f(T_inter). + // Since we can't compute it exactly, we return the following approximation: + // + // f(T_inter) = f(P1) & f(P2) & ... & f(Pn) + // + // The reason for this is the following: In general, for any function 'f', the + // set f(A) & f(B) is *larger than or equal to* the set f(A & B). This means + // that we will return a type that is possibly wider than it could be, but + // never wrong. + // + // However, we do have to leave out the negative contributions. If we were to + // add a contribution like ~f(N1), we would potentially infer result types + // that are too narrow. + // + // As an example for this, consider the intersection type `int & ~Literal[1]`. + // If 'f' would be the `==`-comparison with 2, we obviously can't tell if that + // answer would be true or false, so we need to return `bool`. And indeed, we + // we have (glossing over notational details): + // + // f(int & ~1) + // = f({..., -1, 0, 2, 3, ...}) + // = {..., False, False, True, False, ...} + // = bool + // + // On the other hand, if we were to compute + // + // f(int) & ~f(1) + // = bool & ~False + // = True + // + // we would get a result type `Literal[True]` which is too narrow. + // + let mut builder = IntersectionBuilder::new(db); + + builder = builder.add_positive(KnownClass::Bool.to_instance(db)); + + let mut state = State::NoPositiveElements; + + for pos in intersection.positive(db) { + let result = match intersection_on { + IntersectionOn::Left => { + infer_binary_type_comparison(context, *pos, op, other, range, visitor) + } + IntersectionOn::Right => { + infer_binary_type_comparison(context, other, op, *pos, range, visitor) + } + }; + + match result { + Ok(ty) => { + state = State::Supported; + builder = builder.add_positive(ty); + } + Err(error) => { + match state { + State::NoPositiveElements => { + // This is the first positive element, but the operation is not supported. + // Store the error and continue. + state = State::UnsupportedOnAllElements(error); + } + State::UnsupportedOnAllElements(_) => { + // We already have an error stored, and continue to see elements on which + // the operator is not supported. Continue with the same state (only keep + // the first error). + } + State::Supported => { + // We previously saw a positive element that supported the operator, + // so the overall operation is still supported. + } + } + } + } + } + + match state { + State::Supported => Ok(builder.build()), + State::NoPositiveElements => { + // We didn't see any positive elements, check if the operation is supported on `object`: + match intersection_on { + IntersectionOn::Left => { + infer_binary_type_comparison(context, Type::object(), op, other, range, visitor) + } + IntersectionOn::Right => { + infer_binary_type_comparison(context, other, op, Type::object(), range, visitor) + } + } + } + State::UnsupportedOnAllElements(error) => Err(error), + } +} + +/// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their +/// behaviour can be edited for classes by implementing corresponding dunder methods. +/// This function performs rich comparison between two types and returns the resulting type. +/// see `` +fn infer_rich_comparison<'db>( + db: &'db dyn Db, + left: Type<'db>, + right: Type<'db>, + op: RichCompareOperator, + policy: MemberLookupPolicy, +) -> Result, UnsupportedComparisonError<'db>> { + // The following resource has details about the rich comparison algorithm: + // https://snarky.ca/unravelling-rich-comparison-operators/ + let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| { + left.try_call_dunder_with_policy( + db, + op.dunder(), + &mut CallArguments::positional([right]), + TypeContext::default(), + policy, + ) + .map(|outcome| outcome.return_type(db)) + .ok() + }; + + // The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side. + if left != right && right.is_subtype_of(db, left) { + call_dunder(op.reflect(), right, left).or_else(|| call_dunder(op, left, right)) + } else { + call_dunder(op, left, right).or_else(|| call_dunder(op.reflect(), right, left)) + } + .or_else(|| { + // When no appropriate method returns any value other than NotImplemented, + // the `==` and `!=` operators will fall back to `is` and `is not`, respectively. + // refer to `` + if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) + // This branch implements specific behavior of the `__eq__` and `__ne__` methods + // on `object`, so it does not apply if we skip looking up attributes on `object`. + && !policy.mro_no_object_fallback() + { + Some(KnownClass::Bool.to_instance(db)) + } else { + None + } + }) + .ok_or_else(|| UnsupportedComparisonError { + op: op.into(), + left_ty: left, + right_ty: right, + }) +} + +/// Performs a membership test (`in` and `not in`) between two instances and returns the resulting type, or `None` if the test is unsupported. +/// The behavior can be customized in Python by implementing `__contains__`, `__iter__`, or `__getitem__` methods. +/// See `` +/// and `` +fn infer_membership_test_comparison<'db>( + context: &InferContext<'db, '_>, + left: Type<'db>, + right: Type<'db>, + op: MembershipTestCompareOperator, + range: TextRange, +) -> Result, UnsupportedComparisonError<'db>> { + let db = context.db(); + let compare_result_opt = match right.try_call_dunder( + db, + "__contains__", + CallArguments::positional([left]), + TypeContext::default(), + ) { + // If `__contains__` is available, it is used directly for the membership test. + Ok(bindings) => Some(bindings.return_type(db)), + // If `__contains__` is not available or possibly unbound, + // fall back to iteration-based membership test. + Err(CallDunderError::MethodNotAvailable | CallDunderError::PossiblyUnbound(_)) => right + .try_iterate(db) + .map(|_| KnownClass::Bool.to_instance(db)) + .ok(), + // `__contains__` exists but can't be called with the given arguments. + Err(CallDunderError::CallError(..)) => None, + }; + + compare_result_opt + .map(|ty| { + if matches!(ty, Type::Dynamic(DynamicType::Todo(_))) { + return ty; + } + + let truthiness = ty.try_bool(db).unwrap_or_else(|err| { + err.report_diagnostic(context, range); + err.fallback_truthiness() + }); + + match op { + MembershipTestCompareOperator::In => truthiness.into_type(db), + MembershipTestCompareOperator::NotIn => truthiness.negate().into_type(db), + } + }) + .ok_or_else(|| UnsupportedComparisonError { + op: op.into(), + left_ty: left, + right_ty: right, + }) +} + +/// Simulates rich comparison between tuples and returns the inferred result. +/// This performs a lexicographic comparison, returning a union of all possible return types that could result from the comparison. +/// +/// basically it's based on cpython's `tuple_richcompare` +/// see `` +fn infer_tuple_rich_comparison<'db>( + context: &InferContext<'db, '_>, + left: &TupleSpec<'db>, + op: RichCompareOperator, + right: &TupleSpec<'db>, + range: TextRange, + visitor: &BinaryComparisonVisitor<'db>, +) -> Result, UnsupportedComparisonError<'db>> { + let db = context.db(); + match (left, right) { + // Both fixed-length: perform full lexicographic comparison. + (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) => { + let left_iter = left.iter_all_elements(); + let right_iter = right.iter_all_elements(); + + let mut builder = UnionBuilder::new(db); + + for (l_ty, r_ty) in left_iter.zip(right_iter) { + let pairwise_eq_result = infer_binary_type_comparison( + context, + l_ty, + ast::CmpOp::Eq, + r_ty, + range, + visitor, + ) + .expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); + + match pairwise_eq_result.try_bool(db).unwrap_or_else(|err| { + // TODO: We should, whenever possible, pass the range of the left and right elements + // instead of the range of the whole tuple. + err.report_diagnostic(context, range); + err.fallback_truthiness() + }) { + // - AlwaysTrue : Continue to the next pair for lexicographic comparison + Truthiness::AlwaysTrue => continue, + // - AlwaysFalse: + // Lexicographic comparisons will always terminate with this pair. + // Complete the comparison and return the result. + // - Ambiguous: + // Lexicographic comparisons might continue to the next pair (if eq_result is true), + // or terminate here (if eq_result is false). + // To account for cases where the comparison terminates here, add the pairwise comparison result to the union builder. + eq_truthiness @ (Truthiness::AlwaysFalse | Truthiness::Ambiguous) => { + let pairwise_compare_result = match op { + RichCompareOperator::Lt + | RichCompareOperator::Le + | RichCompareOperator::Gt + | RichCompareOperator::Ge => infer_binary_type_comparison( + context, + l_ty, + op.into(), + r_ty, + range, + visitor, + )?, + // For `==` and `!=`, we already figure out the result from `pairwise_eq_result` + // NOTE: The CPython implementation does not account for non-boolean return types + // or cases where `!=` is not the negation of `==`, we also do not consider these cases. + RichCompareOperator::Eq => Type::bool_literal(false), + RichCompareOperator::Ne => Type::bool_literal(true), + }; + + builder = builder.add(pairwise_compare_result); + + if eq_truthiness.is_ambiguous() { + continue; + } + + return Ok(builder.build()); + } + } + } + + // if no more items to compare, we just compare sizes + let (left_len, right_len) = (left.len(), right.len()); + + builder = builder.add(Type::bool_literal(match op { + RichCompareOperator::Eq => left_len == right_len, + RichCompareOperator::Ne => left_len != right_len, + RichCompareOperator::Lt => left_len < right_len, + RichCompareOperator::Le => left_len <= right_len, + RichCompareOperator::Gt => left_len > right_len, + RichCompareOperator::Ge => left_len >= right_len, + })); + + Ok(builder.build()) + } + + // At least one tuple is variable-length. We can make no assumptions about + // the relative lengths of the tuples, and therefore neither about how they + // compare lexicographically. However, we still need to verify that the + // element types are comparable for ordering comparisons. + + // For equality comparisons (==, !=), any two objects can be compared, + // and tuple equality always returns bool regardless of element __eq__ return types. + (TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) + if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) => + { + Ok(KnownClass::Bool.to_instance(db)) + } + + // At least one variable-length: check all elements that could potentially be compared. + // We use `try_for_each_element_pair` to iterate over all possible pairings. + (left @ TupleSpec::Variable(_), right) | (left, right @ TupleSpec::Variable(_)) => { + let mut results = SmallVec::<[Type<'db>; 8]>::new(); + left.try_for_each_element_pair(right, |l_ty, r_ty| { + results.push(infer_binary_type_comparison( + context, + l_ty, + op.into(), + r_ty, + range, + visitor, + )?); + Ok::<_, UnsupportedComparisonError<'db>>(()) + })?; + + let mut builder = UnionBuilder::new(db); + for result in results { + builder = builder.add(result); + } + // Length comparison (when all elements are equal) returns bool. + builder = builder.add(KnownClass::Bool.to_instance(db)); + + Ok(builder.build()) + } + } +} From 4bd2cdc4659e6c04a47f94cb6898b10eec4b5c97 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 1 Mar 2026 11:08:02 -0500 Subject: [PATCH 142/261] [ty] Limit recursion depth when displaying self-referential function types (#23647) ## Summary Closes https://github.com/astral-sh/ty/issues/2922. --- .../resources/mdtest/ty_extensions.md | 30 +++++++++++++++++++ .../ty_python_semantic/src/types/display.rs | 9 ++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index cd0a4fa342bd6..e20a82ca7b628 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -439,6 +439,36 @@ def foo(x: "TypeOf[foo]"): reveal_type(x) # revealed: def foo(x: def foo(...)) -> Unknown ``` +## Deeply nested `TypeOf` chains + +Multiple redefinitions of a function with `TypeOf[foo]` as the return type create a chain of +distinct function types. The display of such chains is truncated to prevent extremely long output: + +```py +from ty_extensions import TypeOf + +def foo() -> TypeOf[foo]: # error: [unresolved-reference] + return foo + +def foo() -> TypeOf[foo]: + return foo # error: [invalid-return-type] + +def foo() -> TypeOf[foo]: + return foo # error: [invalid-return-type] + +def foo() -> TypeOf[foo]: + return foo # error: [invalid-return-type] + +def foo() -> TypeOf[foo]: + return foo # error: [invalid-return-type] + +def foo() -> TypeOf[foo]: + return foo # error: [invalid-return-type] + +# Truncated after 4 levels of function type nesting: +reveal_type(foo) # revealed: def foo() -> def foo() -> def foo() -> def foo() -> def foo(...) +``` + ## `CallableTypeOf` The `CallableTypeOf` special form can be used to extract the `Callable` structural type inhabited by diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index f29da948a65c1..9f91105d50cec 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1468,8 +1468,13 @@ pub(crate) struct DisplayFunctionType<'db> { impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - // Detect self-referential function types to prevent infinite recursion. - if self.settings.visited_function_types.contains(&self.ty) { + // Detect self-referential function types to prevent infinite recursion, + // and limit display depth for chains of different function types + // (e.g. multiple redefinitions with `TypeOf[foo]` return types). + const MAX_FUNCTION_TYPE_DISPLAY_DEPTH: usize = 4; + if self.settings.visited_function_types.contains(&self.ty) + || self.settings.visited_function_types.len() >= MAX_FUNCTION_TYPE_DISPLAY_DEPTH + { f.set_invalid_type_annotation(); f.write_str("def ")?; write!(f, "{}", self.ty.name(self.db))?; From 63b7ee8accd87e1e6b9bf04218d352fd59d255bd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 1 Mar 2026 17:33:40 -0500 Subject: [PATCH 143/261] [ty] Move binary expression logic out of `builder.rs` (#23649) ## Summary Like #23646. --- .../src/types/infer/builder.rs | 881 +----------------- .../types/infer/builder/binary_expressions.rs | 860 +++++++++++++++++ 2 files changed, 878 insertions(+), 863 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f7e40d280963c..ae02b0f0a78da 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -72,22 +72,22 @@ use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::diagnostic::{ self, ABSTRACT_METHOD_IN_FINAL_CLASS, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, - DATACLASS_FIELD_ORDER, DIVISION_BY_ZERO, DUPLICATE_BASE, DUPLICATE_KW_ONLY, - FINAL_ON_NON_METHOD, FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, - INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, - INVALID_DATACLASS, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, - INVALID_KEY, INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, - INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, - INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, - INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, - INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, - INVALID_TYPE_VARIABLE_DEFAULT, INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, - IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, - PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, - POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, - TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, - UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, - UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, + DATACLASS_FIELD_ORDER, DUPLICATE_BASE, DUPLICATE_KW_ONLY, FINAL_ON_NON_METHOD, + FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, + INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DATACLASS, + INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, INVALID_KEY, + INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, + INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, + INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, + INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, + INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, + INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, MISSING_ARGUMENT, + NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, PARAMETER_ALREADY_ASSIGNED, + POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, + SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, + UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, + UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, + hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, report_call_to_abstract_method, report_cannot_delete_typed_dict_key, @@ -106,7 +106,7 @@ use crate::types::diagnostic::{ report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_shadowed_type_variable, report_unsupported_augmented_assignment, - report_unsupported_base, report_unsupported_binary_operation, report_unsupported_comparison, + report_unsupported_base, report_unsupported_comparison, }; use crate::types::enums::is_enum_class_by_inheritance; use crate::types::function::{ @@ -149,6 +149,7 @@ use crate::unpack::{EvaluationMode, UnpackPosition}; use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet, Program}; mod annotation_expression; +mod binary_expressions; mod paramspec_validation; mod type_expression; @@ -2604,46 +2605,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - /// Raise a diagnostic if the given type cannot be divided by zero. - /// - /// Expects the resolved type of the left side of the binary expression. - fn check_division_by_zero( - &mut self, - node: AnyNodeRef<'_>, - op: ast::Operator, - left: Type<'db>, - ) -> bool { - match left { - Type::LiteralValue(literal) - if matches!( - literal.kind(), - LiteralValueTypeKind::Bool(_) | LiteralValueTypeKind::Int(_) - ) => {} - Type::NominalInstance(instance) - if matches!( - instance.known_class(self.db()), - Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) - ) => {} - _ => return false, - } - - let (op, by_zero) = match op { - ast::Operator::Div => ("divide", "by zero"), - ast::Operator::FloorDiv => ("floor divide", "by zero"), - ast::Operator::Mod => ("reduce", "modulo zero"), - _ => return false, - }; - - if let Some(builder) = self.context.report_lint(&DIVISION_BY_ZERO, node) { - builder.into_diagnostic(format_args!( - "Cannot {op} object of type `{}` {by_zero}", - left.display(self.db()) - )); - } - - true - } - /// Add a binding for the given definition. /// /// Returns the result of the `infer_value_ty` closure, which is called with the declared type @@ -14454,812 +14415,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - fn infer_binary_expression( - &mut self, - binary: &ast::ExprBinOp, - tcx: TypeContext<'db>, - ) -> Type<'db> { - if tcx.is_typealias() { - return self.infer_pep_604_union_type_alias(binary, tcx); - } - - let ast::ExprBinOp { - left, - op, - right, - range: _, - node_index: _, - } = binary; - - let left_ty = self.infer_expression(left, TypeContext::default()); - let right_ty = self.infer_expression(right, TypeContext::default()); - - self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op) - .unwrap_or_else(|| { - report_unsupported_binary_operation(&self.context, binary, left_ty, right_ty, *op); - Type::unknown() - }) - } - - fn infer_pep_604_union_type_alias( - &mut self, - node: &ast::ExprBinOp, - tcx: TypeContext<'db>, - ) -> Type<'db> { - let ast::ExprBinOp { - left, - op, - right, - range: _, - node_index: _, - } = node; - - if *op != ast::Operator::BitOr { - // TODO diagnostic? - return Type::unknown(); - } - - let left_ty = self.infer_expression(left, tcx); - let right_ty = self.infer_expression(right, tcx); - - // TODO this is overly aggressive; if the operands' `__or__` does not actually return a - // `UnionType` at runtime, we should ideally not infer one here. But this is unlikely to be - // a problem in practice: it would require someone having an explicitly annotated - // `TypeAlias`, which uses `X | Y` syntax, where the returned type is not actually a union. - // And attempting to enforce this more tightly showed a lot of potential false positives in - // the ecosystem. - if left_ty.is_equivalent_to(self.db(), right_ty) { - left_ty - } else { - UnionTypeInstance::from_value_expression_types( - self.db(), - [left_ty, right_ty], - self.scope(), - self.typevar_binding_context, - ) - } - } - - /// Maps an operation over each constraint of a constrained `TypeVar`. - /// - /// Returns the original `TypeVar` if each result is equivalent to its input constraint; - /// otherwise returns the union of all results. - fn map_constrained_typevar_constraints( - db: &'db dyn Db, - typevar: Type<'db>, - constraints: TypeVarConstraints<'db>, - mut op: impl FnMut(Type<'db>) -> Option>, - ) -> Option> { - let mut builder = UnionBuilder::new(db); - let mut any_different = false; - - for constraint in constraints.elements(db) { - let result = op(*constraint)?; - if !result.is_equivalent_to(db, *constraint) { - any_different = true; - } - builder = builder.add(result); - } - - Some(if any_different { - builder.build() - } else { - typevar - }) - } - - fn infer_binary_expression_type( - &mut self, - node: AnyNodeRef<'_>, - mut emitted_division_by_zero_diagnostic: bool, - left_ty: Type<'db>, - right_ty: Type<'db>, - op: ast::Operator, - ) -> Option> { - // Check for division by zero; this doesn't change the inferred type for the expression, but - // may emit a diagnostic - if !emitted_division_by_zero_diagnostic - && matches!( - op, - ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod - ) - && right_ty.as_literal_value().is_some_and(|literal| { - literal.as_bool() == Some(false) || literal.as_int() == Some(0) - }) - { - emitted_division_by_zero_diagnostic = self.check_division_by_zero(node, op, left_ty); - } - - let pep_604_unions_allowed = || { - Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 - || self.file().is_stub(self.db()) - || self.scope().scope(self.db()).in_type_checking_block() - }; - - match (left_ty, right_ty, op) { - (Type::Union(lhs_union), rhs, _) => lhs_union.try_map(self.db(), |lhs_element| { - self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - *lhs_element, - rhs, - op, - ) - }), - (lhs, Type::Union(rhs_union), _) => rhs_union.try_map(self.db(), |rhs_element| { - self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - lhs, - *rhs_element, - op, - ) - }), - - (Type::TypeAlias(alias), rhs, _) => self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - alias.value_type(self.db()), - rhs, - op, - ), - - (lhs, Type::TypeAlias(alias), _) => self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - lhs, - alias.value_type(self.db()), - op, - ), - - // Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future, - // the result would then become Any or Unknown, respectively). - (div @ Type::Dynamic(DynamicType::Divergent(_)), _, _) - | (_, div @ Type::Dynamic(DynamicType::Divergent(_)), _) => Some(div), - - (any @ Type::Dynamic(DynamicType::Any), _, _) - | (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any), - - (unknown @ Type::Dynamic(DynamicType::Unknown), _, _) - | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), - - (unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _, _) - | (_, unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _) => Some(unknown), - - (typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _, _) - | (_, typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _) => Some(typevar), - - (todo @ Type::Dynamic(DynamicType::TodoFunctionalTypedDict), _, _) - | (_, todo @ Type::Dynamic(DynamicType::TodoFunctionalTypedDict), _) => Some(todo), - - // When both operands are the same constrained TypeVar (e.g., `T: (int, str)`), - // we check if the operation is valid for each constraint paired with itself. - // This is different from treating it as a union, where we'd check all combinations. - // For example, `T + T` where `T: (int, str)` should check `int + int` and `str + str`, - // not `int + str` which would fail. - // - // If each constraint's operation returns the same type as the constraint (e.g., - // `int + int -> int`), we return the TypeVar to preserve the generic relationship. - // Otherwise, we return the union of the return types. - // - // TODO: We expect to replace this with more general support for handling constrained TypeVars - // in arbitrary method/function calls. - (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar), _) - if left_tvar.identity(self.db()) == right_tvar.identity(self.db()) => - { - match left_tvar.typevar(self.db()).bound_or_constraints(self.db()) { - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - Self::map_constrained_typevar_constraints( - self.db(), - left_ty, - constraints, - |constraint| { - self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - constraint, - constraint, - op, - ) - }, - ) - } - // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op(self.db(), left_ty, op, right_ty) - .map(|outcome| outcome.return_type(self.db())) - .ok(), - } - } - - // When the left operand is a constrained TypeVar (e.g., `T: (int, float)`) and the - // right operand is not a TypeVar, we check if each constraint supports the operation - // with the right operand. For example, `T * 2` where `T: (int, float)` should check - // `int * 2` and `float * 2`, both of which work. - // - // TODO: We expect to replace this with more general support once we migrate to the new - // solver. - (Type::TypeVar(left_tvar), rhs, _) if !rhs.is_type_var() => { - match left_tvar.typevar(self.db()).bound_or_constraints(self.db()) { - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - Self::map_constrained_typevar_constraints( - self.db(), - left_ty, - constraints, - |constraint| { - self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - constraint, - rhs, - op, - ) - }, - ) - } - // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op(self.db(), left_ty, op, right_ty) - .map(|outcome| outcome.return_type(self.db())) - .ok(), - } - } - - // When the right operand is a constrained TypeVar and the left operand is not a TypeVar, - // we check if each constraint supports the operation with the left operand. - (lhs, Type::TypeVar(right_tvar), _) if !lhs.is_type_var() => { - match right_tvar - .typevar(self.db()) - .bound_or_constraints(self.db()) - { - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - Self::map_constrained_typevar_constraints( - self.db(), - right_ty, - constraints, - |constraint| { - self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - lhs, - constraint, - op, - ) - }, - ) - } - // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op(self.db(), left_ty, op, right_ty) - .map(|outcome| outcome.return_type(self.db())) - .ok(), - } - } - - // `try_call_bin_op` works for almost all `NewType`s, but not for `NewType`s of `float` - // and `complex`, where the concrete base type is a union. In that case it turns out - // the `self` types of the dunder methods in typeshed don't match, because they don't - // get the same `int | float` and `int | float | complex` special treatment that the - // positional arguments get. In those cases we need to explicitly delegate to the base - // type, so that it hits the `Type::Union` branches above. - (Type::NewTypeInstance(newtype), rhs, _) => { - Type::try_call_bin_op(self.db(), left_ty, op, right_ty) - .map(|outcome| outcome.return_type(self.db())) - .ok() - .or_else(|| { - self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - newtype.concrete_base_type(self.db()), - rhs, - op, - ) - }) - } - (lhs, Type::NewTypeInstance(newtype), _) => { - Type::try_call_bin_op(self.db(), left_ty, op, right_ty) - .map(|outcome| outcome.return_type(self.db())) - .ok() - .or_else(|| { - self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - lhs, - newtype.concrete_base_type(self.db()), - op, - ) - }) - } - - ( - todo @ Type::Dynamic( - DynamicType::Todo(_) - | DynamicType::TodoUnpack - | DynamicType::TodoStarredExpression - | DynamicType::TodoTypeVarTuple, - ), - _, - _, - ) - | ( - _, - todo @ Type::Dynamic( - DynamicType::Todo(_) - | DynamicType::TodoUnpack - | DynamicType::TodoStarredExpression - | DynamicType::TodoTypeVarTuple, - ), - _, - ) => Some(todo), - - (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), - - (Type::LiteralValue(left), Type::LiteralValue(right), _) => { - match (left.kind(), right.kind(), op) { - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::Add, - ) => Some( - n.as_i64() - .checked_add(m.as_i64()) - .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::Sub, - ) => Some( - n.as_i64() - .checked_sub(m.as_i64()) - .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::Mult, - ) => Some( - n.as_i64() - .checked_mul(m.as_i64()) - .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ), - - ( - LiteralValueTypeKind::Int(_), - LiteralValueTypeKind::Int(_), - ast::Operator::Div, - ) => Some(KnownClass::Float.to_instance(self.db())), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::FloorDiv, - ) => Some({ - let mut q = n.as_i64().checked_div(m.as_i64()); - let r = n.as_i64().checked_rem(m.as_i64()); - // Division works differently in Python than in Rust. If the result is negative and - // there is a remainder, the division rounds down (instead of towards zero): - if n.as_i64().is_negative() != m.as_i64().is_negative() - && r.unwrap_or(0) != 0 - { - q = q.map(|q| q - 1); - } - q.map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())) - }), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::Mod, - ) => Some({ - let mut r = n.as_i64().checked_rem(m.as_i64()); - // Division works differently in Python than in Rust. If the result is negative and - // there is a remainder, the division rounds down (instead of towards zero). Adjust - // the remainder to compensate so that q * m + r == n: - if n.as_i64().is_negative() != m.as_i64().is_negative() - && r.unwrap_or(0) != 0 - { - r = r.map(|x| x + m.as_i64()); - } - r.map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())) - }), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::Pow, - ) => Some({ - if m.as_i64() < 0 { - KnownClass::Float.to_instance(self.db()) - } else { - u32::try_from(m.as_i64()) - .ok() - .and_then(|m| n.as_i64().checked_pow(m)) - .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())) - } - }), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::BitOr, - ) => Some(Type::int_literal(n.as_i64() | m.as_i64())), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::BitAnd, - ) => Some(Type::int_literal(n.as_i64() & m.as_i64())), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::BitXor, - ) => Some(Type::int_literal(n.as_i64() ^ m.as_i64())), - - ( - LiteralValueTypeKind::Bytes(lhs), - LiteralValueTypeKind::Bytes(rhs), - ast::Operator::Add, - ) => { - let bytes = [lhs.value(self.db()), rhs.value(self.db())].concat(); - Some(Type::bytes_literal(self.db(), &bytes)) - } - - ( - LiteralValueTypeKind::String(lhs), - LiteralValueTypeKind::String(rhs), - ast::Operator::Add, - ) => { - let lhs_value = lhs.value(self.db()).to_string(); - let rhs_value = rhs.value(self.db()); - let ty = - if lhs_value.len() + rhs_value.len() <= Self::MAX_STRING_LITERAL_SIZE { - Type::string_literal(self.db(), &(lhs_value + rhs_value)) - } else { - Type::literal_string() - }; - Some(ty) - } - - ( - LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString, - LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString, - ast::Operator::Add, - ) => Some(Type::literal_string()), - - ( - LiteralValueTypeKind::String(s), - LiteralValueTypeKind::Int(n), - ast::Operator::Mult, - ) - | ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::String(s), - ast::Operator::Mult, - ) => { - let ty = if n.as_i64() < 1 { - Type::string_literal(self.db(), "") - } else if let Ok(n) = usize::try_from(n.as_i64()) - && n.checked_mul(s.value(self.db()).len()) - .is_some_and(|new_length| { - new_length <= Self::MAX_STRING_LITERAL_SIZE - }) - { - let new_literal = s.value(self.db()).repeat(n); - Type::string_literal(self.db(), &new_literal) - } else { - Type::literal_string() - }; - Some(ty) - } - - ( - LiteralValueTypeKind::LiteralString, - LiteralValueTypeKind::Int(n), - ast::Operator::Mult, - ) - | ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::LiteralString, - ast::Operator::Mult, - ) => { - let ty = if n.as_i64() < 1 { - Type::string_literal(self.db(), "") - } else { - Type::literal_string() - }; - Some(ty) - } - - ( - LiteralValueTypeKind::Bool(b1), - LiteralValueTypeKind::Bool(b2), - ast::Operator::BitOr, - ) => Some(Type::bool_literal(b1 | b2)), - - ( - LiteralValueTypeKind::Bool(b1), - LiteralValueTypeKind::Bool(b2), - ast::Operator::BitAnd, - ) => Some(Type::bool_literal(b1 & b2)), - - ( - LiteralValueTypeKind::Bool(b1), - LiteralValueTypeKind::Bool(b2), - ast::Operator::BitXor, - ) => Some(Type::bool_literal(b1 ^ b2)), - - ( - LiteralValueTypeKind::Bool(b1), - LiteralValueTypeKind::Bool(_) | LiteralValueTypeKind::Int(_), - op, - ) => self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - Type::int_literal(i64::from(b1)), - right_ty, - op, - ), - - (LiteralValueTypeKind::Int(_), LiteralValueTypeKind::Bool(b2), op) => self - .infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - left_ty, - Type::int_literal(i64::from(b2)), - op, - ), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::LShift, - ) if n.as_i64() == 0 && m.as_i64() >= 0 => Some(Type::int_literal(0)), - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::LShift, - ) => { - let n = n.as_i64(); - - // An additional overflow check beyond `checked_shl` is necessary - // here, because `checked_shl` only rejects shift amounts >= 64; - // it does not detect when significant bits are shifted into (or - // past) the sign bit. For example, `1i64.checked_shl(63)` returns - // `Some(i64::MIN)`, but Python's `1 << 63` is a large positive int. - // - // We compute the "headroom": the number of redundant sign-extension - // bits minus one (for the sign bit itself). A shift is safe iff - // `m <= headroom`. - let headroom = if n >= 0 { - n.leading_zeros().saturating_sub(1) - } else { - n.leading_ones().saturating_sub(1) - }; - Some( - u32::try_from(m.as_i64()) - .ok() - .filter(|&m| m <= headroom) - .and_then(|m| n.checked_shl(m)) - .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ) - } - - ( - LiteralValueTypeKind::Int(n), - LiteralValueTypeKind::Int(m), - ast::Operator::RShift, - ) => { - let n = n.as_i64(); - let result = match u32::try_from(m.as_i64()) { - Ok(m) => Type::int_literal(n >> m.clamp(0, 63)), - Err(_) if m.as_i64() > 0 => { - Type::int_literal(if n >= 0 { 0 } else { -1 }) - } - Err(_) => KnownClass::Int.to_instance(self.db()), - }; - Some(result) - } - - _ => Type::try_call_bin_op(self.db(), left_ty, op, right_ty) - .map(|outcome| outcome.return_type(self.db())) - .ok(), - } - } - - ( - Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), - Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), - ast::Operator::BitAnd, - ) => { - let constraints = ConstraintSetBuilder::new(); - let result = constraints.into_owned(|constraints| { - let left = constraints.load(left.constraints(self.db())); - let right = constraints.load(right.constraints(self.db())); - left.and(self.db(), constraints, || right) - }); - Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( - InternedConstraintSet::new(self.db(), result), - ))) - } - - ( - Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), - Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), - ast::Operator::BitOr, - ) => { - let constraints = ConstraintSetBuilder::new(); - let result = constraints.into_owned(|constraints| { - let left = constraints.load(left.constraints(self.db())); - let right = constraints.load(right.constraints(self.db())); - left.or(self.db(), constraints, || right) - }); - Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( - InternedConstraintSet::new(self.db(), result), - ))) - } - - // PEP 604-style union types using the `|` operator. - ( - Type::ClassLiteral(..) - | Type::SubclassOf(..) - | Type::GenericAlias(..) - | Type::SpecialForm(_) - | Type::KnownInstance( - KnownInstanceType::UnionType(_) - | KnownInstanceType::Literal(_) - | KnownInstanceType::Annotated(_) - | KnownInstanceType::TypeGenericAlias(_) - | KnownInstanceType::Callable(_) - | KnownInstanceType::TypeVar(_), - ), - Type::ClassLiteral(..) - | Type::SubclassOf(..) - | Type::GenericAlias(..) - | Type::SpecialForm(_) - | Type::KnownInstance( - KnownInstanceType::UnionType(_) - | KnownInstanceType::Literal(_) - | KnownInstanceType::Annotated(_) - | KnownInstanceType::TypeGenericAlias(_) - | KnownInstanceType::Callable(_) - | KnownInstanceType::TypeVar(_), - ), - ast::Operator::BitOr, - ) if pep_604_unions_allowed() => { - if left_ty.is_equivalent_to(self.db(), right_ty) { - Some(left_ty) - } else { - Some(UnionTypeInstance::from_value_expression_types( - self.db(), - [left_ty, right_ty], - self.scope(), - self.typevar_binding_context, - )) - } - } - ( - Type::ClassLiteral(..) - | Type::SubclassOf(..) - | Type::GenericAlias(..) - | Type::KnownInstance(..) - | Type::SpecialForm(..), - Type::NominalInstance(instance), - ast::Operator::BitOr, - ) - | ( - Type::NominalInstance(instance), - Type::ClassLiteral(..) - | Type::SubclassOf(..) - | Type::GenericAlias(..) - | Type::KnownInstance(..) - | Type::SpecialForm(..), - ast::Operator::BitOr, - ) if pep_604_unions_allowed() - && instance.has_known_class(self.db(), KnownClass::NoneType) => - { - Some(UnionTypeInstance::from_value_expression_types( - self.db(), - [left_ty, right_ty], - self.scope(), - self.typevar_binding_context, - )) - } - - // We avoid calling `type.__(r)or__`, as typeshed annotates these methods as - // accepting `Any` (since typeforms are inexpressable in the type system currently). - // This means that many common errors would not be caught if we fell back to typeshed's stubs here. - // - // Note that if a class had a custom metaclass that overrode `__(r)or__`, we would also ignore - // that custom method as we'd take one of the earlier branches. - // This seems like it's probably rare enough that it's acceptable, however. - ( - Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..), - _, - ast::Operator::BitOr, - ) - | ( - _, - Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..), - ast::Operator::BitOr, - ) if pep_604_unions_allowed() => Type::try_call_bin_op_with_policy( - self.db(), - left_ty, - ast::Operator::BitOr, - right_ty, - MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, - ) - .ok() - .map(|binding| binding.return_type(self.db())), - - // We've handled all of the special cases that we support for literals, so we need to - // fall back on looking for dunder methods on one of the operand types. - ( - Type::FunctionLiteral(_) - | Type::Callable(..) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::KnownBoundMethod(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::ModuleLiteral(_) - | Type::ClassLiteral(_) - | Type::GenericAlias(_) - | Type::SubclassOf(_) - | Type::NominalInstance(_) - | Type::ProtocolInstance(_) - | Type::SpecialForm(_) - | Type::KnownInstance(_) - | Type::PropertyInstance(_) - | Type::Intersection(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::LiteralValue(_) - | Type::BoundSuper(_) - | Type::TypeVar(_) - | Type::TypeIs(_) - | Type::TypeGuard(_) - | Type::TypedDict(_), - Type::FunctionLiteral(_) - | Type::Callable(..) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::KnownBoundMethod(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::ModuleLiteral(_) - | Type::ClassLiteral(_) - | Type::GenericAlias(_) - | Type::SubclassOf(_) - | Type::NominalInstance(_) - | Type::ProtocolInstance(_) - | Type::SpecialForm(_) - | Type::KnownInstance(_) - | Type::PropertyInstance(_) - | Type::Intersection(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::LiteralValue(_) - | Type::BoundSuper(_) - | Type::TypeVar(_) - | Type::TypeIs(_) - | Type::TypeGuard(_) - | Type::TypedDict(_), - op, - ) => Type::try_call_bin_op(self.db(), left_ty, op, right_ty) - .map(|outcome| outcome.return_type(self.db())) - .ok(), - } - } - fn infer_boolean_expression(&mut self, bool_op: &ast::ExprBoolOp) -> Type<'db> { let ast::ExprBoolOp { range: _, diff --git a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs new file mode 100644 index 0000000000000..e43d880d81b3f --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs @@ -0,0 +1,860 @@ +use ruff_python_ast::{self as ast, AnyNodeRef}; + +use super::TypeInferenceBuilder; +use crate::Db; +use crate::types::constraints::ConstraintSetBuilder; +use crate::types::diagnostic::{DIVISION_BY_ZERO, report_unsupported_binary_operation}; +use crate::types::{ + DynamicType, InternedConstraintSet, KnownClass, KnownInstanceType, LiteralValueTypeKind, + MemberLookupPolicy, Type, TypeContext, TypeVarBoundOrConstraints, TypeVarConstraints, + UnionBuilder, UnionTypeInstance, +}; +use ruff_python_ast::PythonVersion; + +use crate::Program; + +impl<'db> TypeInferenceBuilder<'db, '_> { + pub(super) fn infer_binary_expression( + &mut self, + binary: &ast::ExprBinOp, + tcx: TypeContext<'db>, + ) -> Type<'db> { + if tcx.is_typealias() { + return self.infer_pep_604_union_type_alias(binary, tcx); + } + + let ast::ExprBinOp { + left, + op, + right, + range: _, + node_index: _, + } = binary; + + let left_ty = self.infer_expression(left, TypeContext::default()); + let right_ty = self.infer_expression(right, TypeContext::default()); + + self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op) + .unwrap_or_else(|| { + report_unsupported_binary_operation(&self.context, binary, left_ty, right_ty, *op); + Type::unknown() + }) + } + + fn infer_pep_604_union_type_alias( + &mut self, + node: &ast::ExprBinOp, + tcx: TypeContext<'db>, + ) -> Type<'db> { + let db = self.db(); + let ast::ExprBinOp { + left, + op, + right, + range: _, + node_index: _, + } = node; + + if *op != ast::Operator::BitOr { + // TODO diagnostic? + return Type::unknown(); + } + + let left_ty = self.infer_expression(left, tcx); + let right_ty = self.infer_expression(right, tcx); + + // TODO this is overly aggressive; if the operands' `__or__` does not actually return a + // `UnionType` at runtime, we should ideally not infer one here. But this is unlikely to be + // a problem in practice: it would require someone having an explicitly annotated + // `TypeAlias`, which uses `X | Y` syntax, where the returned type is not actually a union. + // And attempting to enforce this more tightly showed a lot of potential false positives in + // the ecosystem. + if left_ty.is_equivalent_to(db, right_ty) { + left_ty + } else { + UnionTypeInstance::from_value_expression_types( + db, + [left_ty, right_ty], + self.scope(), + self.typevar_binding_context, + ) + } + } + + /// Maps an operation over each constraint of a constrained `TypeVar`. + /// + /// Returns the original `TypeVar` if each result is equivalent to its input constraint; + /// otherwise returns the union of all results. + pub(super) fn map_constrained_typevar_constraints( + db: &'db dyn Db, + typevar: Type<'db>, + constraints: TypeVarConstraints<'db>, + mut op: impl FnMut(Type<'db>) -> Option>, + ) -> Option> { + let mut builder = UnionBuilder::new(db); + let mut any_different = false; + + for constraint in constraints.elements(db) { + let result = op(*constraint)?; + if !result.is_equivalent_to(db, *constraint) { + any_different = true; + } + builder = builder.add(result); + } + + Some(if any_different { + builder.build() + } else { + typevar + }) + } + + pub(super) fn infer_binary_expression_type( + &mut self, + node: AnyNodeRef<'_>, + mut emitted_division_by_zero_diagnostic: bool, + left_ty: Type<'db>, + right_ty: Type<'db>, + op: ast::Operator, + ) -> Option> { + let db = self.db(); + + // Check for division by zero; this doesn't change the inferred type for the expression, but + // may emit a diagnostic + if !emitted_division_by_zero_diagnostic + && matches!( + op, + ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod + ) + && right_ty.as_literal_value().is_some_and(|literal| { + literal.as_bool() == Some(false) || literal.as_int() == Some(0) + }) + { + emitted_division_by_zero_diagnostic = self.check_division_by_zero(node, op, left_ty); + } + + let pep_604_unions_allowed = || { + Program::get(db).python_version(db) >= PythonVersion::PY310 + || self.file().is_stub(db) + || self.scope().scope(db).in_type_checking_block() + }; + + match (left_ty, right_ty, op) { + (Type::Union(lhs_union), rhs, _) => lhs_union.try_map(db, |lhs_element| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + *lhs_element, + rhs, + op, + ) + }), + (lhs, Type::Union(rhs_union), _) => rhs_union.try_map(db, |rhs_element| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + lhs, + *rhs_element, + op, + ) + }), + + (Type::TypeAlias(alias), rhs, _) => self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + alias.value_type(db), + rhs, + op, + ), + + (lhs, Type::TypeAlias(alias), _) => self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + lhs, + alias.value_type(db), + op, + ), + + // Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future, + // the result would then become Any or Unknown, respectively). + (div @ Type::Dynamic(DynamicType::Divergent(_)), _, _) + | (_, div @ Type::Dynamic(DynamicType::Divergent(_)), _) => Some(div), + + (any @ Type::Dynamic(DynamicType::Any), _, _) + | (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any), + + (unknown @ Type::Dynamic(DynamicType::Unknown), _, _) + | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), + + (unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _, _) + | (_, unknown @ Type::Dynamic(DynamicType::UnknownGeneric(_)), _) => Some(unknown), + + (typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _, _) + | (_, typevar @ Type::Dynamic(DynamicType::UnspecializedTypeVar), _) => Some(typevar), + + (todo @ Type::Dynamic(DynamicType::TodoFunctionalTypedDict), _, _) + | (_, todo @ Type::Dynamic(DynamicType::TodoFunctionalTypedDict), _) => Some(todo), + + // When both operands are the same constrained TypeVar (e.g., `T: (int, str)`), + // we check if the operation is valid for each constraint paired with itself. + // This is different from treating it as a union, where we'd check all combinations. + // For example, `T + T` where `T: (int, str)` should check `int + int` and `str + str`, + // not `int + str` which would fail. + // + // If each constraint's operation returns the same type as the constraint (e.g., + // `int + int -> int`), we return the TypeVar to preserve the generic relationship. + // Otherwise, we return the union of the return types. + // + // TODO: We expect to replace this with more general support for handling constrained TypeVars + // in arbitrary method/function calls. + (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar), _) + if left_tvar.identity(db) == right_tvar.identity(db) => + { + match left_tvar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + Self::map_constrained_typevar_constraints( + db, + left_ty, + constraints, + |constraint| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + constraint, + constraint, + op, + ) + }, + ) + } + // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. + _ => Type::try_call_bin_op(db, left_ty, op, right_ty) + .map(|outcome| outcome.return_type(db)) + .ok(), + } + } + + // When the left operand is a constrained TypeVar (e.g., `T: (int, float)`) and the + // right operand is not a TypeVar, we check if each constraint supports the operation + // with the right operand. For example, `T * 2` where `T: (int, float)` should check + // `int * 2` and `float * 2`, both of which work. + // + // TODO: We expect to replace this with more general support once we migrate to the new + // solver. + (Type::TypeVar(left_tvar), rhs, _) if !rhs.is_type_var() => { + match left_tvar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + Self::map_constrained_typevar_constraints( + db, + left_ty, + constraints, + |constraint| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + constraint, + rhs, + op, + ) + }, + ) + } + // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. + _ => Type::try_call_bin_op(db, left_ty, op, right_ty) + .map(|outcome| outcome.return_type(db)) + .ok(), + } + } + + // When the right operand is a constrained TypeVar and the left operand is not a TypeVar, + // we check if each constraint supports the operation with the left operand. + (lhs, Type::TypeVar(right_tvar), _) if !lhs.is_type_var() => { + match right_tvar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + Self::map_constrained_typevar_constraints( + db, + right_ty, + constraints, + |constraint| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + lhs, + constraint, + op, + ) + }, + ) + } + // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. + _ => Type::try_call_bin_op(db, left_ty, op, right_ty) + .map(|outcome| outcome.return_type(db)) + .ok(), + } + } + + // `try_call_bin_op` works for almost all `NewType`s, but not for `NewType`s of `float` + // and `complex`, where the concrete base type is a union. In that case it turns out + // the `self` types of the dunder methods in typeshed don't match, because they don't + // get the same `int | float` and `int | float | complex` special treatment that the + // positional arguments get. In those cases we need to explicitly delegate to the base + // type, so that it hits the `Type::Union` branches above. + (Type::NewTypeInstance(newtype), rhs, _) => { + Type::try_call_bin_op(db, left_ty, op, right_ty) + .map(|outcome| outcome.return_type(db)) + .ok() + .or_else(|| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + newtype.concrete_base_type(db), + rhs, + op, + ) + }) + } + (lhs, Type::NewTypeInstance(newtype), _) => { + Type::try_call_bin_op(db, left_ty, op, right_ty) + .map(|outcome| outcome.return_type(db)) + .ok() + .or_else(|| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + lhs, + newtype.concrete_base_type(db), + op, + ) + }) + } + + ( + todo @ Type::Dynamic( + DynamicType::Todo(_) + | DynamicType::TodoUnpack + | DynamicType::TodoStarredExpression + | DynamicType::TodoTypeVarTuple, + ), + _, + _, + ) + | ( + _, + todo @ Type::Dynamic( + DynamicType::Todo(_) + | DynamicType::TodoUnpack + | DynamicType::TodoStarredExpression + | DynamicType::TodoTypeVarTuple, + ), + _, + ) => Some(todo), + + (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), + + (Type::LiteralValue(left), Type::LiteralValue(right), _) => { + match (left.kind(), right.kind(), op) { + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::Add, + ) => Some( + n.as_i64() + .checked_add(m.as_i64()) + .map(Type::int_literal) + .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + ), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::Sub, + ) => Some( + n.as_i64() + .checked_sub(m.as_i64()) + .map(Type::int_literal) + .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + ), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::Mult, + ) => Some( + n.as_i64() + .checked_mul(m.as_i64()) + .map(Type::int_literal) + .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + ), + + ( + LiteralValueTypeKind::Int(_), + LiteralValueTypeKind::Int(_), + ast::Operator::Div, + ) => Some(KnownClass::Float.to_instance(db)), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::FloorDiv, + ) => Some({ + let mut q = n.as_i64().checked_div(m.as_i64()); + let r = n.as_i64().checked_rem(m.as_i64()); + // Division works differently in Python than in Rust. If the result is negative and + // there is a remainder, the division rounds down (instead of towards zero): + if n.as_i64().is_negative() != m.as_i64().is_negative() + && r.unwrap_or(0) != 0 + { + q = q.map(|q| q - 1); + } + q.map(Type::int_literal) + .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + }), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::Mod, + ) => Some({ + let mut r = n.as_i64().checked_rem(m.as_i64()); + // Division works differently in Python than in Rust. If the result is negative and + // there is a remainder, the division rounds down (instead of towards zero). Adjust + // the remainder to compensate so that q * m + r == n: + if n.as_i64().is_negative() != m.as_i64().is_negative() + && r.unwrap_or(0) != 0 + { + r = r.map(|x| x + m.as_i64()); + } + r.map(Type::int_literal) + .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + }), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::Pow, + ) => Some({ + if m.as_i64() < 0 { + KnownClass::Float.to_instance(db) + } else { + u32::try_from(m.as_i64()) + .ok() + .and_then(|m| n.as_i64().checked_pow(m)) + .map(Type::int_literal) + .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + } + }), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::BitOr, + ) => Some(Type::int_literal(n.as_i64() | m.as_i64())), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::BitAnd, + ) => Some(Type::int_literal(n.as_i64() & m.as_i64())), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::BitXor, + ) => Some(Type::int_literal(n.as_i64() ^ m.as_i64())), + + ( + LiteralValueTypeKind::Bytes(lhs), + LiteralValueTypeKind::Bytes(rhs), + ast::Operator::Add, + ) => { + let bytes = [lhs.value(db), rhs.value(db)].concat(); + Some(Type::bytes_literal(db, &bytes)) + } + + ( + LiteralValueTypeKind::String(lhs), + LiteralValueTypeKind::String(rhs), + ast::Operator::Add, + ) => { + let lhs_value = lhs.value(db).to_string(); + let rhs_value = rhs.value(db); + let ty = + if lhs_value.len() + rhs_value.len() <= Self::MAX_STRING_LITERAL_SIZE { + Type::string_literal(db, &(lhs_value + rhs_value)) + } else { + Type::literal_string() + }; + Some(ty) + } + + ( + LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString, + LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString, + ast::Operator::Add, + ) => Some(Type::literal_string()), + + ( + LiteralValueTypeKind::String(s), + LiteralValueTypeKind::Int(n), + ast::Operator::Mult, + ) + | ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::String(s), + ast::Operator::Mult, + ) => { + let ty = if n.as_i64() < 1 { + Type::string_literal(db, "") + } else if let Ok(n) = usize::try_from(n.as_i64()) + && n.checked_mul(s.value(db).len()).is_some_and(|new_length| { + new_length <= Self::MAX_STRING_LITERAL_SIZE + }) + { + let new_literal = s.value(db).repeat(n); + Type::string_literal(db, &new_literal) + } else { + Type::literal_string() + }; + Some(ty) + } + + ( + LiteralValueTypeKind::LiteralString, + LiteralValueTypeKind::Int(n), + ast::Operator::Mult, + ) + | ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::LiteralString, + ast::Operator::Mult, + ) => { + let ty = if n.as_i64() < 1 { + Type::string_literal(db, "") + } else { + Type::literal_string() + }; + Some(ty) + } + + ( + LiteralValueTypeKind::Bool(b1), + LiteralValueTypeKind::Bool(b2), + ast::Operator::BitOr, + ) => Some(Type::bool_literal(b1 | b2)), + + ( + LiteralValueTypeKind::Bool(b1), + LiteralValueTypeKind::Bool(b2), + ast::Operator::BitAnd, + ) => Some(Type::bool_literal(b1 & b2)), + + ( + LiteralValueTypeKind::Bool(b1), + LiteralValueTypeKind::Bool(b2), + ast::Operator::BitXor, + ) => Some(Type::bool_literal(b1 ^ b2)), + + ( + LiteralValueTypeKind::Bool(b1), + LiteralValueTypeKind::Bool(_) | LiteralValueTypeKind::Int(_), + op, + ) => self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + Type::int_literal(i64::from(b1)), + right_ty, + op, + ), + + (LiteralValueTypeKind::Int(_), LiteralValueTypeKind::Bool(b2), op) => self + .infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + left_ty, + Type::int_literal(i64::from(b2)), + op, + ), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::LShift, + ) if n.as_i64() == 0 && m.as_i64() >= 0 => Some(Type::int_literal(0)), + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::LShift, + ) => { + let n = n.as_i64(); + + // An additional overflow check beyond `checked_shl` is necessary + // here, because `checked_shl` only rejects shift amounts >= 64; + // it does not detect when significant bits are shifted into (or + // past) the sign bit. For example, `1i64.checked_shl(63)` returns + // `Some(i64::MIN)`, but Python's `1 << 63` is a large positive int. + // + // We compute the "headroom": the number of redundant sign-extension + // bits minus one (for the sign bit itself). A shift is safe iff + // `m <= headroom`. + let headroom = if n >= 0 { + n.leading_zeros().saturating_sub(1) + } else { + n.leading_ones().saturating_sub(1) + }; + Some( + u32::try_from(m.as_i64()) + .ok() + .filter(|&m| m <= headroom) + .and_then(|m| n.checked_shl(m)) + .map(Type::int_literal) + .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + ) + } + + ( + LiteralValueTypeKind::Int(n), + LiteralValueTypeKind::Int(m), + ast::Operator::RShift, + ) => { + let n = n.as_i64(); + let result = match u32::try_from(m.as_i64()) { + Ok(m) => Type::int_literal(n >> m.clamp(0, 63)), + Err(_) if m.as_i64() > 0 => { + Type::int_literal(if n >= 0 { 0 } else { -1 }) + } + Err(_) => KnownClass::Int.to_instance(db), + }; + Some(result) + } + + _ => Type::try_call_bin_op(db, left_ty, op, right_ty) + .map(|outcome| outcome.return_type(db)) + .ok(), + } + } + + ( + Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), + Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), + ast::Operator::BitAnd, + ) => { + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + let left = constraints.load(left.constraints(db)); + let right = constraints.load(right.constraints(db)); + left.and(db, constraints, || right) + }); + Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( + InternedConstraintSet::new(db, result), + ))) + } + + ( + Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), + Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), + ast::Operator::BitOr, + ) => { + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + let left = constraints.load(left.constraints(db)); + let right = constraints.load(right.constraints(db)); + left.or(db, constraints, || right) + }); + Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( + InternedConstraintSet::new(db, result), + ))) + } + + // PEP 604-style union types using the `|` operator. + ( + Type::ClassLiteral(..) + | Type::SubclassOf(..) + | Type::GenericAlias(..) + | Type::SpecialForm(_) + | Type::KnownInstance( + KnownInstanceType::UnionType(_) + | KnownInstanceType::Literal(_) + | KnownInstanceType::Annotated(_) + | KnownInstanceType::TypeGenericAlias(_) + | KnownInstanceType::Callable(_) + | KnownInstanceType::TypeVar(_), + ), + Type::ClassLiteral(..) + | Type::SubclassOf(..) + | Type::GenericAlias(..) + | Type::SpecialForm(_) + | Type::KnownInstance( + KnownInstanceType::UnionType(_) + | KnownInstanceType::Literal(_) + | KnownInstanceType::Annotated(_) + | KnownInstanceType::TypeGenericAlias(_) + | KnownInstanceType::Callable(_) + | KnownInstanceType::TypeVar(_), + ), + ast::Operator::BitOr, + ) if pep_604_unions_allowed() => { + if left_ty.is_equivalent_to(db, right_ty) { + Some(left_ty) + } else { + Some(UnionTypeInstance::from_value_expression_types( + db, + [left_ty, right_ty], + self.scope(), + self.typevar_binding_context, + )) + } + } + ( + Type::ClassLiteral(..) + | Type::SubclassOf(..) + | Type::GenericAlias(..) + | Type::KnownInstance(..) + | Type::SpecialForm(..), + Type::NominalInstance(instance), + ast::Operator::BitOr, + ) + | ( + Type::NominalInstance(instance), + Type::ClassLiteral(..) + | Type::SubclassOf(..) + | Type::GenericAlias(..) + | Type::KnownInstance(..) + | Type::SpecialForm(..), + ast::Operator::BitOr, + ) if pep_604_unions_allowed() && instance.has_known_class(db, KnownClass::NoneType) => { + Some(UnionTypeInstance::from_value_expression_types( + db, + [left_ty, right_ty], + self.scope(), + self.typevar_binding_context, + )) + } + + // We avoid calling `type.__(r)or__`, as typeshed annotates these methods as + // accepting `Any` (since typeforms are inexpressable in the type system currently). + // This means that many common errors would not be caught if we fell back to typeshed's stubs here. + // + // Note that if a class had a custom metaclass that overrode `__(r)or__`, we would also ignore + // that custom method as we'd take one of the earlier branches. + // This seems like it's probably rare enough that it's acceptable, however. + ( + Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..), + _, + ast::Operator::BitOr, + ) + | ( + _, + Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..), + ast::Operator::BitOr, + ) if pep_604_unions_allowed() => Type::try_call_bin_op_with_policy( + db, + left_ty, + ast::Operator::BitOr, + right_ty, + MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .ok() + .map(|binding| binding.return_type(db)), + + // We've handled all of the special cases that we support for literals, so we need to + // fall back on looking for dunder methods on one of the operand types. + ( + Type::FunctionLiteral(_) + | Type::Callable(..) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::KnownBoundMethod(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::Intersection(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::LiteralValue(_) + | Type::BoundSuper(_) + | Type::TypeVar(_) + | Type::TypeIs(_) + | Type::TypeGuard(_) + | Type::TypedDict(_), + Type::FunctionLiteral(_) + | Type::Callable(..) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::KnownBoundMethod(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::Intersection(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::LiteralValue(_) + | Type::BoundSuper(_) + | Type::TypeVar(_) + | Type::TypeIs(_) + | Type::TypeGuard(_) + | Type::TypedDict(_), + op, + ) => Type::try_call_bin_op(db, left_ty, op, right_ty) + .map(|outcome| outcome.return_type(db)) + .ok(), + } + } + + /// Raise a diagnostic if the given type cannot be divided by zero. + /// + /// Expects the resolved type of the left side of the binary expression. + fn check_division_by_zero( + &mut self, + node: AnyNodeRef<'_>, + op: ast::Operator, + left: Type<'db>, + ) -> bool { + let db = self.db(); + match left { + Type::LiteralValue(literal) + if matches!( + literal.kind(), + LiteralValueTypeKind::Bool(_) | LiteralValueTypeKind::Int(_) + ) => {} + Type::NominalInstance(instance) + if matches!( + instance.known_class(db), + Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) + ) => {} + _ => return false, + } + + let (op, by_zero) = match op { + ast::Operator::Div => ("divide", "by zero"), + ast::Operator::FloorDiv => ("floor divide", "by zero"), + ast::Operator::Mod => ("reduce", "modulo zero"), + _ => return false, + }; + + if let Some(builder) = self.context.report_lint(&DIVISION_BY_ZERO, node) { + builder.into_diagnostic(format_args!( + "Cannot {op} object of type `{}` {by_zero}", + left.display(db) + )); + } + + true + } +} From c8f8f5ede57b039e54c6d612ae4e44ea59c1370c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:46:25 +0000 Subject: [PATCH 144/261] Update dependency ruff to v0.15.4 (#23660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [ruff](https://docs.astral.sh/ruff) ([source](https://redirect.github.com/astral-sh/ruff), [changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md)) | `==0.15.2` → `==0.15.4` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.15.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.15.2/0.15.4?slim=true) | --- ### Release Notes

astral-sh/ruff (ruff) ### [`v0.15.4`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0154) [Compare Source](https://redirect.github.com/astral-sh/ruff/compare/0.15.3...0.15.4) Released on 2026-02-26. This is a follow-up release to 0.15.3 that resolves a panic when the new rule `PLR1712` was enabled with any rule that analyzes definitions, such as many of the `ANN` or `D` rules. ##### Bug fixes - Fix panic on access to definitions after analyzing definitions ([#​23588](https://redirect.github.com/astral-sh/ruff/pull/23588)) - \[`pyflakes`] Suppress false positive in `F821` for names used before `del` in stub files ([#​23550](https://redirect.github.com/astral-sh/ruff/pull/23550)) ##### Documentation - Clarify first-party import detection in Ruff ([#​23591](https://redirect.github.com/astral-sh/ruff/pull/23591)) - Fix incorrect `import-heading` example ([#​23568](https://redirect.github.com/astral-sh/ruff/pull/23568)) ##### Contributors - [@​stakeswky](https://redirect.github.com/stakeswky) - [@​ntBre](https://redirect.github.com/ntBre) - [@​thejcannon](https://redirect.github.com/thejcannon) - [@​GeObts](https://redirect.github.com/GeObts) ### [`v0.15.3`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0153) [Compare Source](https://redirect.github.com/astral-sh/ruff/compare/0.15.2...0.15.3) Released on 2026-02-26. ##### Preview features - Drop explicit support for `.qmd` file extension ([#​23572](https://redirect.github.com/astral-sh/ruff/pull/23572)) This can now be enabled instead by setting the [`extension`](https://docs.astral.sh/ruff/settings/#extension) option: ```toml # ruff.toml extension = { qmd = "markdown" } # pyproject.toml [tool.ruff] extension = { qmd = "markdown" } ``` - Include configured extensions in file discovery ([#​23400](https://redirect.github.com/astral-sh/ruff/pull/23400)) - \[`flake8-bandit`] Allow suspicious imports in `TYPE_CHECKING` blocks (`S401`-`S415`) ([#​23441](https://redirect.github.com/astral-sh/ruff/pull/23441)) - \[`flake8-bugbear`] Allow `B901` in pytest hook wrappers ([#​21931](https://redirect.github.com/astral-sh/ruff/pull/21931)) - \[`flake8-import-conventions`] Add missing conventions from upstream (`ICN001`, `ICN002`) ([#​21373](https://redirect.github.com/astral-sh/ruff/pull/21373)) - \[`pydocstyle`] Add rule to enforce docstring section ordering (`D420`) ([#​23537](https://redirect.github.com/astral-sh/ruff/pull/23537)) - \[`pylint`] Implement `swap-with-temporary-variable` (`PLR1712`) ([#​22205](https://redirect.github.com/astral-sh/ruff/pull/22205)) - \[`ruff`] Add `unnecessary-assign-before-yield` (`RUF070`) ([#​23300](https://redirect.github.com/astral-sh/ruff/pull/23300)) - \[`ruff`] Support file-level noqa in `RUF102` ([#​23535](https://redirect.github.com/astral-sh/ruff/pull/23535)) - \[`ruff`] Suppress diagnostic for invalid f-strings before Python 3.12 (`RUF027`) ([#​23480](https://redirect.github.com/astral-sh/ruff/pull/23480)) - \[`flake8-bandit`] Don't flag `BaseLoader`/`CBaseLoader` as unsafe (`S506`) ([#​23510](https://redirect.github.com/astral-sh/ruff/pull/23510)) ##### Bug fixes - Avoid infinite loop between `I002` and `PYI025` ([#​23352](https://redirect.github.com/astral-sh/ruff/pull/23352)) - \[`pyflakes`] Fix false positive for `@overload` from `lint.typing-modules` (`F811`) ([#​23357](https://redirect.github.com/astral-sh/ruff/pull/23357)) - \[`pyupgrade`] Fix false positive for `TypeVar` default before Python 3.12 (`UP046`) ([#​23540](https://redirect.github.com/astral-sh/ruff/pull/23540)) - \[`pyupgrade`] Fix handling of `\N` in raw strings (`UP032`) ([#​22149](https://redirect.github.com/astral-sh/ruff/pull/22149)) ##### Rule changes - Render sub-diagnostics in the GitHub output format ([#​23455](https://redirect.github.com/astral-sh/ruff/pull/23455)) - \[`flake8-bugbear`] Tag certain `B007` diagnostics as unnecessary ([#​23453](https://redirect.github.com/astral-sh/ruff/pull/23453)) - \[`ruff`] Ignore unknown rule codes in `RUF100` ([#​23531](https://redirect.github.com/astral-sh/ruff/pull/23531)) These are now flagged by [`RUF102`](https://docs.astral.sh/ruff/rules/invalid-rule-code/) instead. ##### Documentation - Fix missing settings links for several linters ([#​23519](https://redirect.github.com/astral-sh/ruff/pull/23519)) - Update isort action comments heading ([#​23515](https://redirect.github.com/astral-sh/ruff/pull/23515)) - \[`pydocstyle`] Fix double comma in description of `D404` ([#​23440](https://redirect.github.com/astral-sh/ruff/pull/23440)) ##### Other changes - Update the Python module (notably `find_ruff_bin`) for parity with uv ([#​23406](https://redirect.github.com/astral-sh/ruff/pull/23406)) ##### Contributors - [@​zanieb](https://redirect.github.com/zanieb) - [@​o1x3](https://redirect.github.com/o1x3) - [@​assadyousuf](https://redirect.github.com/assadyousuf) - [@​kar-ganap](https://redirect.github.com/kar-ganap) - [@​denyszhak](https://redirect.github.com/denyszhak) - [@​amyreese](https://redirect.github.com/amyreese) - [@​carljm](https://redirect.github.com/carljm) - [@​anishgirianish](https://redirect.github.com/anishgirianish) - [@​Bnyro](https://redirect.github.com/Bnyro) - [@​danparizher](https://redirect.github.com/danparizher) - [@​ntBre](https://redirect.github.com/ntBre) - [@​gcomneno](https://redirect.github.com/gcomneno) - [@​jaap3](https://redirect.github.com/jaap3) - [@​stakeswky](https://redirect.github.com/stakeswky)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index c4d6cfc0354e4..b6c22898e996c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.3 -ruff==0.15.2 +ruff==0.15.4 mkdocs==1.6.1 mkdocs-material==9.7.1 mkdocs-redirects==1.2.2 From 75301bb9b08e72ae407efbc9ff7522fe917c4664 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:46:41 +0000 Subject: [PATCH 145/261] Update astral-sh/setup-uv action to v7.3.1 (#23657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) | action | patch | `v7.3.0` → `v7.3.1` | --- ### Release Notes
astral-sh/setup-uv (astral-sh/setup-uv) ### [`v7.3.1`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v7.3.1): 🌈 fall back to VERSION_CODENAME when VERSION_ID is not available [Compare Source](https://redirect.github.com/astral-sh/setup-uv/compare/v7.3.0...v7.3.1) ##### Changes This release adds support for running in containers like `debian:testing` or `debian:unstable` ##### 🐛 Bug fixes - fix: fall back to VERSION\_CODENAME when VERSION\_ID is not available [@​eifinger-bot](https://redirect.github.com/eifinger-bot) ([#​774](https://redirect.github.com/astral-sh/setup-uv/issues/774)) ##### 🧰 Maintenance - chore: update known checksums for 0.10.6 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​771](https://redirect.github.com/astral-sh/setup-uv/issues/771)) - chore: update known checksums for 0.10.5 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​770](https://redirect.github.com/astral-sh/setup-uv/issues/770)) - chore: update known checksums for 0.10.4 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​768](https://redirect.github.com/astral-sh/setup-uv/issues/768)) - chore: update known checksums for 0.10.3 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​767](https://redirect.github.com/astral-sh/setup-uv/issues/767)) - chore: update known checksums for 0.10.2 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​765](https://redirect.github.com/astral-sh/setup-uv/issues/765)) - chore: update known checksums for 0.10.1 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​764](https://redirect.github.com/astral-sh/setup-uv/issues/764)) ##### ⬆️ Dependency updates - Bump github/codeql-action from 4.31.9 to 4.32.2 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​766](https://redirect.github.com/astral-sh/setup-uv/issues/766)) - Bump zizmorcore/zizmor-action from 0.4.1 to 0.5.0 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​763](https://redirect.github.com/astral-sh/setup-uv/issues/763))
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 28 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/mypy_primer.yaml | 4 +-- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/publish-versions.yml | 2 +- .github/workflows/sync_typeshed.yaml | 6 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- .github/workflows/ty-ecosystem-report.yaml | 2 +- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c91cff06ba1c1..a2e58f1bbbc72 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -289,7 +289,7 @@ jobs: with: tool: cargo-insta - name: "Install uv" - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" enable-cache: "true" @@ -348,7 +348,7 @@ jobs: with: tool: cargo-nextest - name: "Install uv" - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" enable-cache: "true" @@ -382,7 +382,7 @@ jobs: with: tool: cargo-nextest - name: "Install uv" - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" enable-cache: "true" @@ -489,7 +489,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 @@ -526,7 +526,7 @@ jobs: - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - name: "Install Rust toolchain" @@ -568,7 +568,7 @@ jobs: ref: ${{ github.event.pull_request.base.ref }} persist-credentials: false - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true @@ -676,7 +676,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 @@ -737,7 +737,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 @@ -790,7 +790,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 @@ -826,7 +826,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: Install uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: python-version: 3.13 activate-environment: true @@ -979,7 +979,7 @@ jobs: - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" @@ -1060,7 +1060,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" @@ -1111,7 +1111,7 @@ jobs: - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" @@ -1155,7 +1155,7 @@ jobs: with: persist-credentials: false - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index b0dabeea5b4d1..5c2094c65eed2 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - name: "Install Rust toolchain" diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 6531b1dd7a252..d69a8ef57c6cd 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" @@ -97,7 +97,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 90433e868017d..3f1c45b446c40 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -22,7 +22,7 @@ jobs: id-token: write steps: - name: "Install uv" - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml index e0d6c9e4cac5d..81dc5fcecfa7c 100644 --- a/.github/workflows/publish-versions.yml +++ b/.github/workflows/publish-versions.yml @@ -30,7 +30,7 @@ jobs: run: git clone https://${{ secrets.ASTRAL_VERSIONS_PAT }}@github.com/astral-sh/versions.git astral-versions - name: "Install uv" - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 - name: "Update versions" env: diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index b24216f45410c..94b57c3d52fc1 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -76,7 +76,7 @@ jobs: run: | git config --global user.name typeshedbot git config --global user.email '<>' - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - name: Sync typeshed stubs @@ -132,7 +132,7 @@ jobs: with: persist-credentials: true ref: ${{ env.UPSTREAM_BRANCH}} - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - name: Setup git @@ -173,7 +173,7 @@ jobs: with: persist-credentials: true ref: ${{ env.UPSTREAM_BRANCH}} - - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.4" - name: Setup git diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index e39c0a1e8eed2..be1aecb2ce24d 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: enable-cache: true version: "0.10.4" diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 589e87e299bf1..6460ba1561e4a 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -32,7 +32,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: enable-cache: true version: "0.10.4" From 5261968772e9369745f9fbea67c73cee5eac3707 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:46:54 +0000 Subject: [PATCH 146/261] Update dependency astral-sh/uv to v0.10.7 (#23658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [astral-sh/uv](https://redirect.github.com/astral-sh/uv) | uses-with | patch | `0.10.4` → `0.10.7` | --- ### Release Notes
astral-sh/uv (astral-sh/uv) ### [`v0.10.7`](https://redirect.github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0107) [Compare Source](https://redirect.github.com/astral-sh/uv/compare/0.10.6...0.10.7) Released on 2026-02-27. ##### Bug fixes - Fix handling of junctions in Windows Containers on Windows ([#​18192](https://redirect.github.com/astral-sh/uv/pull/18192)) ##### Enhancements - Activate logging for middleware retries ([#​18200](https://redirect.github.com/astral-sh/uv/pull/18200)) - Upload uv releases to a mirror ([#​18159](https://redirect.github.com/astral-sh/uv/pull/18159)) ### [`v0.10.6`](https://redirect.github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0106) [Compare Source](https://redirect.github.com/astral-sh/uv/compare/0.10.5...0.10.6) Released on 2026-02-24. ##### Bug fixes - Apply lockfile marker normalization for fork markers ([#​18116](https://redirect.github.com/astral-sh/uv/pull/18116)) - Fix Python version selection for scripts with a `requires-python` conflicting with `.python-version` ([#​18097](https://redirect.github.com/astral-sh/uv/pull/18097)) - Preserve file permissions when using reflinks on Linux ([#​18187](https://redirect.github.com/astral-sh/uv/pull/18187)) ##### Documentation - Remove verbose documentation from optional dependencies help text ([#​18180](https://redirect.github.com/astral-sh/uv/pull/18180)) ### [`v0.10.5`](https://redirect.github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0105) [Compare Source](https://redirect.github.com/astral-sh/uv/compare/0.10.4...0.10.5) Released on 2026-02-23. ##### Enhancements - Add hint when named index is found in a parent config file ([#​18087](https://redirect.github.com/astral-sh/uv/pull/18087)) - Add warning for `uv lock --frozen` ([#​17859](https://redirect.github.com/astral-sh/uv/pull/17859)) - Attempt to use reflinks by default on Linux ([#​18117](https://redirect.github.com/astral-sh/uv/pull/18117)) - Fallback to hardlinks after reflink failure before copying ([#​18104](https://redirect.github.com/astral-sh/uv/pull/18104)) - Filter `pylock.toml` wheels by tags and `requires-python` ([#​18081](https://redirect.github.com/astral-sh/uv/pull/18081)) - Validate wheel filenames are normalized during `uv publish` ([#​17783](https://redirect.github.com/astral-sh/uv/pull/17783)) - Fix message when `exclude-newer` invalidates the lock file ([#​18100](https://redirect.github.com/astral-sh/uv/pull/18100)) - Change the missing files log level to debug ([#​18075](https://redirect.github.com/astral-sh/uv/pull/18075)) ##### Performance - Improve performance of repeated conflicts with an extra ([#​18094](https://redirect.github.com/astral-sh/uv/pull/18094)) ##### Bug fixes - Fix `--no-emit-workspace` with `--all-packages` on single-member workspaces ([#​18098](https://redirect.github.com/astral-sh/uv/pull/18098)) - Fix `UV_NO_DEFAULT_GROUPS` rejecting truthy values like `1` ([#​18057](https://redirect.github.com/astral-sh/uv/pull/18057)) - Fix iOS detection ([#​17973](https://redirect.github.com/astral-sh/uv/pull/17973)) - Propagate project-level conflicts to package extras ([#​18096](https://redirect.github.com/astral-sh/uv/pull/18096)) - Use a global build concurrency semaphore ([#​18054](https://redirect.github.com/astral-sh/uv/pull/18054)) ##### Documentation - Update documentation heading for environment variable files ([#​18122](https://redirect.github.com/astral-sh/uv/pull/18122)) - Fix comment about `uv export` formats ([#​17900](https://redirect.github.com/astral-sh/uv/pull/17900)) - Make it clear that Windows is supported in user- and system- level configuration docs ([#​18106](https://redirect.github.com/astral-sh/uv/pull/18106))
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 28 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/mypy_primer.yaml | 4 +-- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/sync_typeshed.yaml | 6 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- .github/workflows/ty-ecosystem-report.yaml | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a2e58f1bbbc72..05565d36ef0d3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -291,7 +291,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -350,7 +350,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -384,7 +384,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" enable-cache: "true" - name: "Run tests" run: | @@ -491,7 +491,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: shared-key: ruff-linux-debug @@ -528,7 +528,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -572,7 +572,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.10.4" + version: "0.10.7" - name: "Install Rust toolchain" run: rustup show @@ -678,7 +678,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -739,7 +739,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -792,7 +792,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 24 @@ -830,7 +830,7 @@ jobs: with: python-version: 3.13 activate-environment: true - version: "0.10.4" + version: "0.10.7" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -981,7 +981,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: "Install Rust toolchain" run: rustup show @@ -1062,7 +1062,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: "Install codspeed" uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 @@ -1113,7 +1113,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: "Install Rust toolchain" run: rustup show @@ -1157,7 +1157,7 @@ jobs: - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: "Install codspeed" uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index 5c2094c65eed2..77dae0e6532f5 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -36,7 +36,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index d69a8ef57c6cd..0d3f89dff29f3 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -54,7 +54,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: @@ -99,7 +99,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 3f1c45b446c40..36891e94189a4 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -24,7 +24,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: wheels-* diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 94b57c3d52fc1..1d5b06dcbd06e 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -78,7 +78,7 @@ jobs: git config --global user.email '<>' - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -134,7 +134,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: Setup git run: | git config --global user.name typeshedbot @@ -175,7 +175,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.4" + version: "0.10.7" - name: Setup git run: | git config --global user.name typeshedbot diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index be1aecb2ce24d..d34246f646b4b 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -41,7 +41,7 @@ jobs: uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: enable-cache: true - version: "0.10.4" + version: "0.10.7" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 6460ba1561e4a..3e23cd88d456d 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -35,7 +35,7 @@ jobs: uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: enable-cache: true - version: "0.10.4" + version: "0.10.7" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: From ee277469d3bddb0d5fd59e308263059561be1e0a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:47:58 +0000 Subject: [PATCH 147/261] Update Rust crate jiff to v0.2.21 (#23665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | Pending | |---|---|---|---|---| | [jiff](https://redirect.github.com/BurntSushi/jiff) | workspace.dependencies | patch | `0.2.20` → `0.2.21` | `0.2.22` | --- ### Release Notes
BurntSushi/jiff (jiff) ### [`v0.2.21`](https://redirect.github.com/BurntSushi/jiff/blob/HEAD/CHANGELOG.md#0221-2026-02-22) [Compare Source](https://redirect.github.com/BurntSushi/jiff/compare/0.2.20...0.2.21) \=================== This release contains a performance improvement and a bug fix for `civil::Date::new` where it could panic on some inputs. Bug fixes: - [#​523](https://redirect.github.com/BurntSushi/jiff/issues/523): Fix a bug where `Date::new` could panic. This was a regression introduced in `jiff 0.2.20`. Performance: - [#​518](https://redirect.github.com/BurntSushi/jiff/pull/518): Improve `Timestamp` to `civil::DateTime` conversion performance by \~15%.
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 094de23b51d8e..5898dab1c71f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,7 +710,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1169,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1853,9 +1853,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1863,14 +1863,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" dependencies = [ "proc-macro2", "quote", @@ -3761,7 +3761,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4178,7 +4178,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5403,7 +5403,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] From 2296c36612ce0e1293d76cd5a9795c25366fddc2 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Mon, 2 Mar 2026 09:46:01 +0000 Subject: [PATCH 148/261] Publish releases to Astral mirror (#23616) --- .github/workflows/publish-mirror.yml | 45 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 11 +++++++ dist-workspace.toml | 4 +-- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-mirror.yml diff --git a/.github/workflows/publish-mirror.yml b/.github/workflows/publish-mirror.yml new file mode 100644 index 0000000000000..2d260487a8d06 --- /dev/null +++ b/.github/workflows/publish-mirror.yml @@ -0,0 +1,45 @@ +# Publish ruff releases to a mirror +# +# Assumed to run as a subworkflow of .github/workflows/release.yml as a custom publish job +name: publish-mirror + +on: + workflow_call: + inputs: + plan: + required: true + type: string + +permissions: {} + +jobs: + publish-mirror: + runs-on: ubuntu-latest + environment: + name: release + env: + VERSION: ${{ fromJson(inputs.plan).announcement_tag }} + steps: + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: "Upload to R2" + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MIRROR_R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MIRROR_R2_SECRET_ACCESS_KEY }} + AWS_ENDPOINT_URL: https://${{ secrets.MIRROR_R2_CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com + AWS_DEFAULT_REGION: auto + R2_BUCKET: ${{ secrets.MIRROR_R2_BUCKET_NAME }} + PROJECT: ruff + run: | + aws s3 cp --recursive --output table --color on \ + --exclude '*' \ + --include '*.zip' --include '*.zip.sha256' \ + --include '*.tar.gz' --include '*.tar.gz.sha256' \ + --include sha256.sum --include '*.ps1' --include '*.sh' \ + --cache-control "public, max-age=31536000, immutable" \ + artifacts/ \ + "s3://${R2_BUCKET}/github/${PROJECT}/releases/download/${VERSION}/" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 938b16210611f..0db982bd5b8e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -338,3 +338,14 @@ jobs: with: plan: ${{ needs.plan.outputs.val }} secrets: inherit + + custom-publish-mirror: + needs: + - plan + - announce + uses: ./.github/workflows/publish-mirror.yml + with: + plan: ${{ needs.plan.outputs.val }} + secrets: inherit + permissions: + "contents": "read" diff --git a/dist-workspace.toml b/dist-workspace.toml index b79f6f2728f73..2f8b22644847e 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -60,9 +60,9 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker", "./build-wasm"] # Publish jobs to run in CI publish-jobs = ["./publish-pypi", "./publish-wasm"] # Post-announce jobs to run in CI -post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground", "./publish-versions"] +post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground", "./publish-versions", "./publish-mirror"] # Custom permissions for GitHub Jobs -github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read", id-token = "write", attestations = "write" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } } +github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read", id-token = "write", attestations = "write" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" }, "publish-mirror" = { contents = "read" } } # Whether to install an updater program install-updater = false # Path that installers should place binaries in From 9566f3be237d800bb7533bf2959841be2a8ac72b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:18:11 +0000 Subject: [PATCH 149/261] Update Rust crate syn to v2.0.117 (#23667) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5898dab1c71f4..bbfcadea1189e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4142,9 +4142,9 @@ checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", From 3add0268b5464e18342f75dac550496ba89d5d72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:18:41 +0000 Subject: [PATCH 150/261] Update taiki-e/install-action action to v2.68.8 (#23672) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 05565d36ef0d3..c2370ae0df84d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -281,11 +281,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-insta - name: "Install uv" @@ -344,7 +344,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-nextest - name: "Install uv" @@ -378,7 +378,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-nextest - name: "Install uv" @@ -987,7 +987,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-codspeed @@ -1026,7 +1026,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-codspeed @@ -1065,7 +1065,7 @@ jobs: version: "0.10.7" - name: "Install codspeed" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-codspeed @@ -1119,7 +1119,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-codspeed @@ -1160,7 +1160,7 @@ jobs: version: "0.10.7" - name: "Install codspeed" - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 with: tool: cargo-codspeed From 1fc3aff61f0bb4aecdacdd056d0b9d37935c53f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:28:20 +0000 Subject: [PATCH 151/261] Update dependency mkdocs-material to v9.7.2 (#23659) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index b6c22898e996c..b2065045778bd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ PyYAML==6.0.3 ruff==0.15.4 mkdocs==1.6.1 -mkdocs-material==9.7.1 +mkdocs-material==9.7.2 mkdocs-redirects==1.2.2 mdformat==1.0.0 mdformat-mkdocs==5.1.4 From 8f0607ac9fd709125ae01ab367fc9dfe0df5c1e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:28:52 +0000 Subject: [PATCH 152/261] Update extractions/setup-just action to v3.1.0 (#23669) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2370ae0df84d..9c662213e2ac6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -871,7 +871,7 @@ jobs: needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} steps: - - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3.0.0 + - uses: extractions/setup-just@f8a3cce218d9f83db3a2ecd90e41ac3de6cdfd9b # v3.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f33eb3fe6ff20c48d1276431bb820e77ca75ca06 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:29:10 +0000 Subject: [PATCH 153/261] Update Rust crate strum to 0.28.0 (#23670) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Waygood --- .github/renovate.json5 | 2 +- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 0ba62bd5d1653..f54e2f89ae1be 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -95,7 +95,7 @@ { groupName: "strum", matchManagers: ["cargo"], - matchPackageNames: ["strum"], + matchPackageNames: ["strum", "strum_macros"], description: "Weekly update of strum dependencies", } ], diff --git a/Cargo.lock b/Cargo.lock index bbfcadea1189e..83d37f9574ec0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,7 +580,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -710,7 +710,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1077,7 +1077,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1169,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1863,7 +1863,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -3761,7 +3761,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -4115,18 +4115,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", @@ -4178,7 +4178,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -5403,7 +5403,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6a6ea32f0a6cd..1a7e482d661d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,8 +180,8 @@ snapbox = { version = "1.0.0", features = [ "examples", ] } static_assertions = "1.1.0" -strum = { version = "0.27.0", features = ["strum_macros"] } -strum_macros = { version = "0.27.0" } +strum = { version = "0.28.0", features = ["strum_macros"] } +strum_macros = { version = "0.28.0" } supports-hyperlinks = { version = "3.1.0" } syn = { version = "2.0.55" } tempfile = { version = "3.9.0" } From 843c7100590b17a6185dd4ca837c8e20d20f3c5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:31:07 +0100 Subject: [PATCH 154/261] Update Rust crate pyproject-toml to v0.13.7 (#23666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pyproject-toml](https://redirect.github.com/PyO3/pyproject-toml-rs) | workspace.dependencies | patch | `0.13.5` → `0.13.7` | --- ### Release Notes
PyO3/pyproject-toml-rs (pyproject-toml) ### [`v0.13.7`](https://redirect.github.com/PyO3/pyproject-toml-rs/blob/HEAD/Changelog.md#0137) [Compare Source](https://redirect.github.com/PyO3/pyproject-toml-rs/compare/v0.13.6...v0.13.7) - Normalize extra names in optional dependencies ### [`v0.13.6`](https://redirect.github.com/PyO3/pyproject-toml-rs/blob/HEAD/Changelog.md#0136) [Compare Source](https://redirect.github.com/PyO3/pyproject-toml-rs/compare/v0.13.5...v0.13.6) - Support resolving optional dependencies and dependency groups - Update toml to 0.9
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 58 ++++++++++++++---------------------------------------- 1 file changed, 15 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83d37f9574ec0..6d826beade753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,7 +273,7 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.10.5", "log", "prettyplease", "proc-macro2", @@ -2715,7 +2715,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.6", + "toml_edit", ] [[package]] @@ -2740,16 +2740,16 @@ dependencies = [ [[package]] name = "pyproject-toml" -version = "0.13.5" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0f6160dc48298b9260d9b958ad1d7f96f6cd0b9df200b22329204e09334663" +checksum = "f6d755483ad14b49e76713b52285235461a5b4f73f17612353e11a5de36a5fd2" dependencies = [ "indexmap", "pep440_rs", "pep508_rs", "serde", "thiserror 2.0.18", - "toml 0.8.23", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -3945,15 +3945,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.4" @@ -4372,14 +4363,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.23" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] @@ -4390,22 +4384,13 @@ checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.4", + "serde_spanned", "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "toml_writer", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -4424,19 +4409,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "winnow", -] - [[package]] name = "toml_edit" version = "0.23.6" From de67f502d6833ef6218fbd05eddb6acae1c5937d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:31:32 +0000 Subject: [PATCH 155/261] Update Rust crate anyhow to v1.0.102 (#23662) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d826beade753..5a2e580e9ad49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "approx" From db07bc9a99c7fecaa132b8ed44d07197a8818205 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:32:18 +0000 Subject: [PATCH 156/261] Update Rust crate unicode-ident to v1.0.24 (#23668) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a2e580e9ad49..af26ff325ff40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4983,9 +4983,9 @@ checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" From 38d082a03ded899e05e229022fe1e9612f70465f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:53:04 +0000 Subject: [PATCH 157/261] Update Rust crate clap to v4.5.60 (#23663) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af26ff325ff40..7d21d772113b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,9 +501,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.58" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -511,9 +511,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.58" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream 0.6.21", "anstyle", @@ -580,7 +580,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -710,7 +710,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1077,7 +1077,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -1169,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1863,7 +1863,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -3761,7 +3761,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -4169,7 +4169,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -5375,7 +5375,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] From 50035e8545784353a0bab5ffe075e349169d458e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Mar 2026 11:54:00 +0000 Subject: [PATCH 158/261] fix renovate `actions/*-artifact` updates (#23675) --- .github/renovate.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index f54e2f89ae1be..40c49c9865d8f 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -55,7 +55,7 @@ groupName: "Artifact GitHub Actions dependencies", matchManagers: ["github-actions"], matchDatasources: ["gitea-tags", "github-tags"], - matchPackageNames: ["actions/.*-artifact"], + matchPackageNames: ["actions/upload-artifact", "actions/download-artifact"], description: "Weekly update of artifact-related GitHub Actions dependencies", }, { From 04ed24be923b89d5ee305d769353bf162d3f123c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:55:20 +0000 Subject: [PATCH 159/261] Update Rust crate clearscreen to v4.0.5 (#23664) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d21d772113b3..b30d76d552183 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,11 +572,11 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clearscreen" -version = "4.0.3" +version = "4.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1430e4fe087fa90b9fc465ddbe00b994df4dd2c8a05f8fd5e43815bbf541b2dc" +checksum = "5def4343d62f01f67ff1a49147e4a15112e936c6a6a3f8ff7a29394e76468244" dependencies = [ - "nix 0.30.1", + "nix 0.31.1", "terminfo", "thiserror 2.0.18", "which", From af3c21bb9251a3b04f92624d63dd84243e73202d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:07:49 +0000 Subject: [PATCH 160/261] Update actions/attest-build-provenance to 4.1.0 (#23654) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Waygood --- .github/workflows/release.yml | 2 +- dist-workspace.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0db982bd5b8e1..57a03eb25ec8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -283,7 +283,7 @@ jobs: # Remove the granular manifests rm -f artifacts/*-dist-manifest.json - name: Attest - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 with: subject-path: | artifacts/*.json diff --git a/dist-workspace.toml b/dist-workspace.toml index 2f8b22644847e..222098a878418 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -75,4 +75,4 @@ global = "depot-ubuntu-latest-4" "actions/checkout" = "de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2 "actions/upload-artifact" = "b7c566a772e6b6bfb58ed0dc250532a479d7789f" # v6.0.0 "actions/download-artifact" = "37930b1c2abaa49bbe596cd826c3c89aef350131" # v7.0.0 -"actions/attest-build-provenance" = "00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8" # v3.1.0 +"actions/attest-build-provenance" = "a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32" # v4.1.0 From 080644e96cf6de5a4d9dd72fd9a45a53da4f83b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:16:21 +0000 Subject: [PATCH 161/261] Update Artifact GitHub Actions dependencies (#23676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | actions/download-artifact | action | digest | `37930b1` → `70fc10c` | | actions/upload-artifact | action | digest | `b7c566a` → `bbbca2d` | --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Waygood --- .github/workflows/publish-mirror.yml | 2 +- .github/workflows/release.yml | 18 +++++++++--------- dist-workspace.toml | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/publish-mirror.yml b/.github/workflows/publish-mirror.yml index 2d260487a8d06..76a4da4c3ff16 100644 --- a/.github/workflows/publish-mirror.yml +++ b/.github/workflows/publish-mirror.yml @@ -21,7 +21,7 @@ jobs: VERSION: ${{ fromJson(inputs.plan).announcement_tag }} steps: - name: "Download GitHub Artifacts" - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 with: pattern: artifacts-* path: artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57a03eb25ec8c..623be9b9ff49e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,7 +70,7 @@ jobs: shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh" - name: Cache dist - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: cargo-dist-cache path: ~/.cargo/bin/dist @@ -86,7 +86,7 @@ jobs: cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json @@ -140,14 +140,14 @@ jobs: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 with: pattern: artifacts-* path: target/distrib/ @@ -165,7 +165,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: artifacts-build-global path: | @@ -192,14 +192,14 @@ jobs: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 with: pattern: artifacts-* path: target/distrib/ @@ -213,7 +213,7 @@ jobs: cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: # Overwrite the previous copy name: artifacts-dist-manifest @@ -273,7 +273,7 @@ jobs: submodules: recursive # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 with: pattern: artifacts-* path: artifacts diff --git a/dist-workspace.toml b/dist-workspace.toml index 222098a878418..62da0f95d1e48 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -73,6 +73,6 @@ global = "depot-ubuntu-latest-4" [dist.github-action-commits] "actions/checkout" = "de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2 -"actions/upload-artifact" = "b7c566a772e6b6bfb58ed0dc250532a479d7789f" # v6.0.0 -"actions/download-artifact" = "37930b1c2abaa49bbe596cd826c3c89aef350131" # v7.0.0 +"actions/upload-artifact" = "bbbca2ddaa5d8feaa63e36b76fdaad77386f024f" # v7.0.0 +"actions/download-artifact" = "70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3" # v8.0.0 "actions/attest-build-provenance" = "a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32" # v4.1.0 From c20cb8fc39878be34c3f56dc9b1982e13dbf52ac Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 2 Mar 2026 07:28:27 -0500 Subject: [PATCH 162/261] [ty] Move subscript logic out of `builder.rs` (#23653) Co-authored-by: Alex Waygood --- .../src/types/infer/builder.rs | 1032 +---------------- .../src/types/infer/builder/subscript.rs | 981 ++++++++++++++++ 2 files changed, 1003 insertions(+), 1010 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/infer/builder/subscript.rs diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ae02b0f0a78da..cc60203aaf2fd 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1,8 +1,8 @@ use std::borrow::Cow; -use itertools::{Either, EitherOrBoth, Itertools}; +use itertools::{Either, Itertools}; use ruff_db::diagnostic::{ - Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, + Annotation, DiagnosticId, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, }; use ruff_db::files::File; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -60,7 +60,7 @@ use crate::semantic_index::{ place_table, }; use crate::types::builder::RecursivelyDefined; -use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex}; +use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::class::{ AbstractMethod, ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral, @@ -82,23 +82,23 @@ use crate::types::diagnostic::{ INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, MISSING_ARGUMENT, - NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, PARAMETER_ALREADY_ASSIGNED, - POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, - SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, - UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, + NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE, + POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, + TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, + UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, + UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, report_call_to_abstract_method, report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict, report_conflicting_metaclass_from_bases, report_duplicate_bases, report_implicit_return_type, report_instance_layout_conflict, - report_invalid_arguments_to_annotated, report_invalid_assignment, - report_invalid_attribute_assignment, report_invalid_class_match_pattern, - report_invalid_exception_caught, report_invalid_exception_cause, - report_invalid_exception_raised, report_invalid_exception_tuple_caught, - report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, - report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_total_ordering, + report_invalid_assignment, report_invalid_attribute_assignment, + report_invalid_class_match_pattern, report_invalid_exception_caught, + report_invalid_exception_cause, report_invalid_exception_raised, + report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, + report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, + report_invalid_return_type, report_invalid_total_ordering, report_invalid_type_checking_constant, report_invalid_type_param_order, report_invalid_typevar_default_reference, report_match_pattern_against_non_runtime_checkable_protocol, @@ -114,16 +114,13 @@ use crate::types::function::{ OverloadLiteral, function_body_kind, is_implicit_classmethod, }; use crate::types::generics::{ - GenericContext, InferableTypeVars, SpecializationBuilder, bind_typevar, - enclosing_generic_contexts, typing_self, + InferableTypeVars, SpecializationBuilder, bind_typevar, enclosing_generic_contexts, typing_self, }; use crate::types::infer::builder::paramspec_validation::validate_paramspec_components; use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function}; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; -use crate::types::special_form::AliasSpec; use crate::types::subclass_of::SubclassOfInner; -use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType}; use crate::types::typed_dict::{ TypedDictAssignmentKind, TypedDictKeyAssignment, validate_typed_dict_constructor, @@ -131,26 +128,27 @@ use crate::types::typed_dict::{ }; use crate::types::visitor::find_over_type; use crate::types::{ - BoundTypeVarIdentity, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, - CallableTypeKind, ClassType, DataclassParams, DynamicType, GenericAlias, InternedConstraintSet, - InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, + BoundTypeVarIdentity, CallDunderError, CallableBinding, CallableType, CallableTypeKind, + ClassType, DataclassParams, DynamicType, GenericAlias, InternedConstraintSet, InternedType, + IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, LiteralValueTypeKind, ManualPEP695TypeAliasType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, - UnionType, UnionTypeInstance, any_over_type, binding_type, definition_expression_type, - infer_complete_scope_types, infer_scope_types, todo_type, + UnionType, binding_type, definition_expression_type, infer_complete_scope_types, + infer_scope_types, todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; -use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet, Program}; +use crate::{AnalysisSettings, Db, FxIndexSet, Program}; mod annotation_expression; mod binary_expressions; mod paramspec_validation; +mod subscript; mod type_expression; use super::comparisons::{self, BinaryComparisonVisitor}; @@ -14565,992 +14563,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) } - fn infer_subscript_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { - let ast::ExprSubscript { - value, - slice, - range: _, - node_index: _, - ctx, - } = subscript; - - match ctx { - ExprContext::Load => self.infer_subscript_load(subscript), - ExprContext::Store => { - let value_ty = self.infer_expression(value, TypeContext::default()); - let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); - Type::Never - } - ExprContext::Del => { - let value_ty = self.infer_expression(value, TypeContext::default()); - let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.validate_subscript_deletion(subscript, value_ty, slice_ty); - Type::Never - } - ExprContext::Invalid => { - let value_ty = self.infer_expression(value, TypeContext::default()); - let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); - Type::unknown() - } - } - } - - fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { - let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); - - // If we have an implicit type alias like `MyList = list[T]`, and if `MyList` is being - // used in another implicit type alias like `Numbers = MyList[int]`, then we infer the - // right hand side as a value expression, and need to handle the specialization here. - if value_ty.is_generic_alias() { - return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); - } - - self.infer_subscript_load_impl(value_ty, subscript) - } - - fn infer_subscript_load_impl( - &mut self, - value_ty: Type<'db>, - subscript: &ast::ExprSubscript, - ) -> Type<'db> { - let ast::ExprSubscript { - range: _, - node_index: _, - value: _, - slice, - ctx, - } = subscript; - - let mut constraint_keys = vec![]; - - // If `value` is a valid reference, we attempt type narrowing by assignment. - if !value_ty.is_unknown() { - if let Some(expr) = PlaceExpr::try_from_expr(subscript) { - let (place, keys) = self.infer_place_load( - PlaceExprRef::from(&expr), - ast::ExprRef::Subscript(subscript), - ); - constraint_keys.extend(keys); - if let Place::Defined(DefinedPlace { - ty, - definedness: Definedness::AlwaysDefined, - .. - }) = place.place - { - // Even if we can obtain the subscript type based on the assignments, we still perform default type inference - // (to store the expression type and to report errors). - let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); - return ty; - } - } - } - - let tuple_generic_alias = |db: &'db dyn Db, tuple: Option>| { - let tuple = tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown())); - Type::from(tuple.to_class_type(db)) - }; - - match value_ty { - Type::ClassLiteral(class) => { - // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the - // subscript inference logic and treat this as an explicit specialization. - // TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return - // this callable as the `__class_getitem__` method on `type`. That probably requires - // updating all of the subscript logic below to use custom callables for all of the _other_ - // special cases, too. - if class.is_tuple(self.db()) { - return tuple_generic_alias( - self.db(), - self.infer_tuple_type_expression(subscript), - ); - } else if class.is_known(self.db(), KnownClass::Type) { - let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( - InternedType::new(self.db(), argument_ty), - )); - } - - if let Some(generic_context) = class.generic_context(self.db()) - && let Some(class) = class.as_static() - { - return self.infer_explicit_class_specialization( - subscript, - value_ty, - class, - generic_context, - ); - } - } - Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695( - _, - ))) => { - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); - let generic_context = GenericContext::from_typevar_instances(self.db(), variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } - Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { - if let Some(generic_context) = type_alias.generic_context(self.db()) { - return self.infer_explicit_type_alias_type_specialization( - subscript, - value_ty, - type_alias, - generic_context, - ); - } - } - Type::SpecialForm(special_form) => match special_form { - SpecialFormType::Tuple => { - return tuple_generic_alias( - self.db(), - self.infer_tuple_type_expression(subscript), - ); - } - SpecialFormType::Literal => match self.infer_literal_parameter_type(slice) { - Ok(result) => { - return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( - self.db(), - result, - ))); - } - Err(nodes) => { - for node in nodes { - let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) - else { - continue; - }; - builder.into_diagnostic( - "Type arguments for `Literal` must be `None`, \ - a literal value (int, bool, str, or bytes), or an enum member", - ); - } - return Type::unknown(); - } - }, - SpecialFormType::Annotated => { - let ast::Expr::Tuple(ast::ExprTuple { - elts: ref arguments, - .. - }) = **slice - else { - report_invalid_arguments_to_annotated(&self.context, subscript); - - return self.infer_expression(slice, TypeContext::default()); - }; - - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } - - let [type_expr, metadata @ ..] = &arguments[..] else { - for argument in arguments { - self.infer_expression(argument, TypeContext::default()); - } - self.store_expression_type(slice, Type::unknown()); - return Type::unknown(); - }; - - for element in metadata { - self.infer_expression(element, TypeContext::default()); - } - - let ty = self.infer_type_expression(type_expr); - - return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( - self.db(), - ty, - ))); - } - SpecialFormType::Optional => { - let db = self.db(); - - if matches!(**slice, ast::Expr::Tuple(_)) - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "`typing.Optional` requires exactly one argument" - )); - } - - let ty = self.infer_type_expression(slice); - - // `Optional[None]` is equivalent to `None`: - if ty.is_none(db) { - return ty; - } - - return Type::KnownInstance(KnownInstanceType::UnionType( - UnionTypeInstance::new( - db, - None, - Ok(UnionType::from_two_elements(db, ty, Type::none(db))), - ), - )); - } - SpecialFormType::Union => { - let db = self.db(); - - match **slice { - ast::Expr::Tuple(ref tuple) => { - let mut elements = tuple - .elts - .iter() - .map(|elt| self.infer_type_expression(elt)) - .peekable(); - - let is_empty = elements.peek().is_none(); - let union_type = Type::KnownInstance(KnownInstanceType::UnionType( - UnionTypeInstance::new( - db, - None, - Ok(UnionType::from_elements(db, elements)), - ), - )); - - if is_empty - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic( - "`typing.Union` requires at least one type argument", - ); - } - - return union_type; - } - _ => { - return self.infer_expression(slice, TypeContext::default()); - } - } - } - SpecialFormType::Type => { - // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` - let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( - InternedType::new(self.db(), argument_ty), - )); - } - SpecialFormType::Callable => { - let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { - &*tuple.elts - } else { - std::slice::from_ref(&*subscript.slice) - }; - - // TODO: Remove this once we support Concatenate properly. This is necessary - // to avoid a lot of false positives downstream, because we can't represent the typevar- - // specialized `Callable` types yet. - let num_arguments = arguments.len(); - if num_arguments == 2 { - let first_arg = &arguments[0]; - let second_arg = &arguments[1]; - - if first_arg.is_subscript_expr() { - let first_arg_ty = - self.infer_expression(first_arg, TypeContext::default()); - if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) = - first_arg_ty - { - let mut variables = generic_context - .variables(self.db()) - .collect::>(); - - let return_ty = - self.infer_expression(second_arg, TypeContext::default()); - return_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); - - let generic_context = - GenericContext::from_typevar_instances(self.db(), variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } - - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "The first argument to `Callable` must be either a list of types, \ - ParamSpec, Concatenate, or `...`", - )); - } - return Type::KnownInstance(KnownInstanceType::Callable( - CallableType::unknown(self.db()), - )); - } - } - - let callable = self - .infer_callable_type(subscript) - .as_callable() - .expect("always returns Type::Callable"); - - return Type::KnownInstance(KnownInstanceType::Callable(callable)); - } - SpecialFormType::LegacyStdlibAlias(alias) => { - let AliasSpec { - class, - expected_argument_number, - } = alias.alias_spec(); - - let args = if let ast::Expr::Tuple(t) = &**slice { - &*t.elts - } else { - std::slice::from_ref(&**slice) - }; - - if args.len() != expected_argument_number { - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - let noun = if expected_argument_number == 1 { - "argument" - } else { - "arguments" - }; - builder.into_diagnostic(format_args!( - "`typing.{name}` requires exactly \ - {expected_argument_number} {noun}, got {got}", - name = special_form.name(), - got = args.len() - )); - } - } - - let arg_types: Vec<_> = args - .iter() - .map(|arg| self.infer_type_expression(arg)) - .collect(); - - return class - .to_specialized_class_type(self.db(), arg_types) - .map(Type::from) - .unwrap_or_else(Type::unknown); - } - _ => {} - }, - - Type::KnownInstance( - KnownInstanceType::UnionType(_) - | KnownInstanceType::Annotated(_) - | KnownInstanceType::Callable(_) - | KnownInstanceType::TypeGenericAlias(_), - ) => { - return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); - } - Type::Dynamic(DynamicType::Unknown) => { - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); - let generic_context = GenericContext::from_typevar_instances(self.db(), variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } - _ => {} - } - - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); - self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys) - } - - fn infer_explicit_class_specialization( - &mut self, - subscript: &ast::ExprSubscript, - value_ty: Type<'db>, - generic_class: StaticClassLiteral<'db>, - generic_context: GenericContext<'db>, - ) -> Type<'db> { - let db = self.db(); - let specialize = &|types: &[Option>]| { - Type::from(generic_class.apply_specialization(db, |_| { - generic_context.specialize_partial(db, types.iter().copied()) - })) - }; - - self.infer_explicit_callable_specialization( - subscript, - value_ty, - generic_context, - specialize, - ) - } - - fn infer_explicit_type_alias_type_specialization( - &mut self, - subscript: &ast::ExprSubscript, - value_ty: Type<'db>, - generic_type_alias: TypeAliasType<'db>, - generic_context: GenericContext<'db>, - ) -> Type<'db> { - let db = self.db(); - let specialize = &|types: &[Option>]| { - let type_alias = generic_type_alias.apply_specialization(db, |_| { - generic_context.specialize_partial(db, types.iter().copied()) - }); - - Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) - }; - - self.infer_explicit_callable_specialization( - subscript, - value_ty, - generic_context, - specialize, - ) - } - - fn infer_explicit_callable_specialization( - &mut self, - subscript: &ast::ExprSubscript, - value_ty: Type<'db>, - generic_context: GenericContext<'db>, - specialize: &dyn Fn(&[Option>]) -> Type<'db>, - ) -> Type<'db> { - enum ExplicitSpecializationError { - InvalidParamSpec, - UnsatisfiedBound, - UnsatisfiedConstraints, - /// These two errors override the errors above, causing all specializations to be `Unknown`. - MissingTypeVars, - TooManyArguments, - /// This error overrides the errors above, causing the type itself to be `Unknown`. - NonGeneric, - } - - fn add_typevar_definition<'db>( - db: &'db dyn Db, - diagnostic: &mut Diagnostic, - typevar: BoundTypeVarInstance<'db>, - ) { - let Some(definition) = typevar.typevar(db).definition(db) else { - return; - }; - let file = definition.file(db); - let module = parsed_module(db, file).load(db); - let range = definition.focus_range(db, &module).range(); - diagnostic.annotate( - Annotation::secondary(Span::from(file).with_range(range)) - .message("Type variable defined here"), - ); - } - - let db = self.db(); - let constraints = ConstraintSetBuilder::new(); - let slice_node = subscript.slice.as_ref(); - - let exactly_one_paramspec = generic_context.exactly_one_paramspec(db); - let (type_arguments, store_inferred_type_arguments) = match slice_node { - ast::Expr::Tuple(tuple) => { - if exactly_one_paramspec && !tuple.elts.is_empty() { - (std::slice::from_ref(slice_node), false) - } else { - (tuple.elts.as_slice(), true) - } - } - _ => (std::slice::from_ref(slice_node), false), - }; - let mut inferred_type_arguments = Vec::with_capacity(type_arguments.len()); - - let typevars = generic_context.variables(db); - let typevars_len = typevars.len(); - - let mut specialization_types = Vec::with_capacity(typevars_len); - let mut typevar_with_defaults = 0; - let mut missing_typevars = vec![]; - let mut first_excess_type_argument_index = None; - - // Helper to get the AST node corresponding to the type argument at `index`. - let get_node = |index: usize| -> ast::AnyNodeRef<'_> { - match slice_node { - ast::Expr::Tuple(ast::ExprTuple { elts, .. }) if !exactly_one_paramspec => elts - .get(index) - .expect("type argument index should not be out of range") - .into(), - _ => slice_node.into(), - } - }; - - let mut error: Option = None; - - for (index, item) in typevars.zip_longest(type_arguments.iter()).enumerate() { - match item { - EitherOrBoth::Both(typevar, expr) => { - if typevar.default_type(db).is_some() { - typevar_with_defaults += 1; - } - - let provided_type = if typevar.is_paramspec(db) { - match self.infer_paramspec_explicit_specialization_value( - expr, - exactly_one_paramspec, - ) { - Ok(paramspec_value) => paramspec_value, - Err(()) => { - error = Some(ExplicitSpecializationError::InvalidParamSpec); - Type::paramspec_value_callable(db, Parameters::unknown()) - } - } - } else { - self.infer_type_expression(expr) - }; - - inferred_type_arguments.push(provided_type); - - // TODO consider just accepting the given specialization without checking - // against bounds/constraints, but recording the expression for deferred - // checking at end of scope. This would avoid a lot of cycles caused by eagerly - // doing assignment checks here. - match typevar.typevar(db).bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - if provided_type - .when_assignable_to( - db, - bound, - &constraints, - InferableTypeVars::None, - ) - .is_never_satisfied(db) - { - let node = get_node(index); - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Type `{}` is not assignable to upper bound `{}` \ - of type variable `{}`", - provided_type.display(db), - bound.display(db), - typevar.identity(db).display(db), - )); - add_typevar_definition(db, &mut diagnostic, typevar); - } - error = Some(ExplicitSpecializationError::UnsatisfiedBound); - specialization_types.push(Some(Type::unknown())); - } else { - specialization_types.push(Some(provided_type)); - } - } - Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { - // TODO: this is wrong, the given specialization needs to be assignable - // to _at least one_ of the individual constraints, not to the union of - // all of them. `int | str` is not a valid specialization of a typevar - // constrained to `(int, str)`. - if provided_type - .when_assignable_to( - db, - typevar_constraints.as_type(db), - &constraints, - InferableTypeVars::None, - ) - .is_never_satisfied(db) - { - let node = get_node(index); - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Type `{}` does not satisfy constraints `{}` \ - of type variable `{}`", - provided_type.display(db), - typevar_constraints - .elements(db) - .iter() - .map(|c| c.display(db)) - .format("`, `"), - typevar.identity(db).display(db), - )); - add_typevar_definition(db, &mut diagnostic, typevar); - } - error = Some(ExplicitSpecializationError::UnsatisfiedConstraints); - specialization_types.push(Some(Type::unknown())); - } else { - specialization_types.push(Some(provided_type)); - } - } - None => { - specialization_types.push(Some(provided_type)); - } - } - } - EitherOrBoth::Left(typevar) => { - if typevar.default_type(db).is_none() { - // This is an error case, so no need to push into the specialization types. - missing_typevars.push(typevar); - } else { - typevar_with_defaults += 1; - specialization_types.push(None); - } - } - EitherOrBoth::Right(expr) => { - inferred_type_arguments.push(self.infer_type_expression(expr)); - first_excess_type_argument_index.get_or_insert(index); - } - } - } - - if !missing_typevars.is_empty() { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, subscript) { - let description = CallableDescription::new(db, value_ty); - let s = if missing_typevars.len() > 1 { "s" } else { "" }; - builder.into_diagnostic(format_args!( - "No type argument{s} provided for required type variable{s} `{}`{}", - missing_typevars - .iter() - .map(|tv| tv.typevar(db).name(db)) - .format("`, `"), - if let Some(CallableDescription { kind, name }) = description { - format!(" of {kind} `{name}`") - } else { - String::new() - } - )); - } - error = Some(ExplicitSpecializationError::MissingTypeVars); - } - - if let Some(first_excess_type_argument_index) = first_excess_type_argument_index { - if let Type::GenericAlias(alias) = value_ty - && let spec = alias.specialization(self.db()) - && spec - .types(self.db()) - .contains(&Type::Dynamic(DynamicType::TodoTypeVarTuple)) - { - // Avoid false-positive errors when specializing a class - // that's generic over a legacy TypeVarTuple - } else if typevars_len == 0 { - // Type parameter list cannot be empty, so if we reach here, `value_ty` is not a generic type. - if let Some(builder) = self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot subscript non-generic type `{}`", - value_ty.display(db) - )); - if match value_ty { - Type::GenericAlias(_) => true, - Type::KnownInstance(KnownInstanceType::UnionType(union)) => union - .value_expression_types(db) - .is_ok_and(|mut tys| tys.any(|ty| ty.is_generic_alias())), - _ => false, - } { - diagnostic.annotate( - self.context - .secondary(&*subscript.value) - .message("Type is already specialized"), - ); - } - } - error = Some(ExplicitSpecializationError::NonGeneric); - } else { - let node = get_node(first_excess_type_argument_index); - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) { - let description = CallableDescription::new(db, value_ty); - builder.into_diagnostic(format_args!( - "Too many type arguments{}: expected {}, got {}", - if let Some(CallableDescription { kind, name }) = description { - format!(" to {kind} `{name}`") - } else { - String::new() - }, - if typevar_with_defaults == 0 { - format!("{typevars_len}") - } else { - format!( - "between {} and {}", - typevars_len - typevar_with_defaults, - typevars_len - ) - }, - type_arguments.len(), - )); - } - error = Some(ExplicitSpecializationError::TooManyArguments); - } - } - - if store_inferred_type_arguments { - self.store_expression_type( - slice_node, - Type::heterogeneous_tuple(db, inferred_type_arguments), - ); - } - - match error { - Some(ExplicitSpecializationError::NonGeneric) => Type::unknown(), - Some( - ExplicitSpecializationError::MissingTypeVars - | ExplicitSpecializationError::TooManyArguments, - ) => { - let unknowns = generic_context - .variables(self.db()) - .map(|typevar| { - Some(if typevar.is_paramspec(db) { - Type::paramspec_value_callable(db, Parameters::unknown()) - } else { - Type::unknown() - }) - }) - .collect::>(); - specialize(&unknowns) - } - Some( - ExplicitSpecializationError::UnsatisfiedBound - | ExplicitSpecializationError::UnsatisfiedConstraints - | ExplicitSpecializationError::InvalidParamSpec, - ) - | None => specialize(&specialization_types), - } - } - - fn infer_subscript_expression_types( - &self, - subscript: &ast::ExprSubscript, - value_ty: Type<'db>, - slice_ty: Type<'db>, - expr_context: ExprContext, - ) -> Type<'db> { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - enum LegacyGenericContextError<'db> { - /// It's invalid to subscript `Generic` or `Protocol` with this type. - InvalidArgument(Type<'db>), - /// It's invalid to subscript `Generic` or `Protocol` with a variadic tuple type. - /// We should emit a diagnostic for this, but we don't yet. - VariadicTupleArguments, - /// It's valid to subscribe `Generic` or `Protocol` with this type, - /// but the type is not yet supported. - NotYetSupported, - /// A duplicate typevar was provided. - DuplicateTypevar(&'db str), - /// A `TypeVarTuple` was provided but not unpacked. - TypeVarTupleMustBeUnpacked, - } - - impl<'db> LegacyGenericContextError<'db> { - const fn into_type(self) -> Type<'db> { - match self { - LegacyGenericContextError::InvalidArgument(_) - | LegacyGenericContextError::VariadicTupleArguments - | LegacyGenericContextError::DuplicateTypevar(_) - | LegacyGenericContextError::TypeVarTupleMustBeUnpacked => Type::unknown(), - LegacyGenericContextError::NotYetSupported => { - todo_type!("ParamSpecs and TypeVarTuples") - } - } - } - } - - let db = self.db(); - - let legacy_generic_class_context = - |typevars: Type<'db>| -> Result, LegacyGenericContextError<'db>> { - let typevars_class_tuple_spec = typevars.exact_tuple_instance_spec(db); - - let typevars = if let Some(tuple_spec) = typevars_class_tuple_spec.as_deref() { - match tuple_spec { - Tuple::Fixed(typevars) => typevars.elements_slice(), - Tuple::Variable(_) => { - return Err(LegacyGenericContextError::VariadicTupleArguments); - } - } - } else { - std::slice::from_ref(&typevars) - }; - - let mut validated_typevars = FxOrderSet::default(); - for ty in typevars { - let argument_ty = *ty; - if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = argument_ty { - let bound = bind_typevar( - db, - self.index, - self.scope().file_scope_id(db), - self.typevar_binding_context, - typevar, - ) - .ok_or(LegacyGenericContextError::InvalidArgument(argument_ty))?; - if !validated_typevars.insert(bound) { - return Err(LegacyGenericContextError::DuplicateTypevar( - typevar.name(db), - )); - } - } else if let Type::NominalInstance(instance) = argument_ty - && instance.has_known_class(db, KnownClass::TypeVarTuple) - { - return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked); - } else if any_over_type(db, argument_ty, true, |inner_ty| match inner_ty { - Type::Dynamic( - DynamicType::TodoUnpack | DynamicType::TodoStarredExpression, - ) => true, - Type::NominalInstance(nominal) => { - nominal.has_known_class(db, KnownClass::TypeVarTuple) - } - _ => false, - }) { - return Err(LegacyGenericContextError::NotYetSupported); - } else { - return Err(LegacyGenericContextError::InvalidArgument(argument_ty)); - } - } - Ok(GenericContext::from_typevar_instances( - db, - validated_typevars, - )) - }; - - // Special typing forms for which subscriptions are context-dependent are parsed here, - // outside of `Type::subscript`, which is a pure function that doesn't depend on the - // semantic index or any context-dependent state. - let subscript_result = match value_ty { - Type::SpecialForm(SpecialFormType::Generic) => { - match legacy_generic_class_context(slice_ty) { - Ok(context) => Ok(Type::KnownInstance(KnownInstanceType::SubscriptedGeneric( - context, - ))), - Err(LegacyGenericContextError::InvalidArgument(argument_ty)) => { - Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::InvalidLegacyGenericArgument { - origin: LegacyGenericOrigin::Generic, - argument_ty, - }, - )) - } - Err(LegacyGenericContextError::DuplicateTypevar(typevar_name)) => { - Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::DuplicateTypevar { - origin: LegacyGenericOrigin::Generic, - typevar_name, - }, - )) - } - Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked) => { - Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::TypeVarTupleNotUnpacked { - origin: LegacyGenericOrigin::Generic, - }, - )) - } - Err( - error @ (LegacyGenericContextError::NotYetSupported - | LegacyGenericContextError::VariadicTupleArguments), - ) => Ok(error.into_type()), - } - } - Type::SpecialForm(SpecialFormType::Protocol) => { - match legacy_generic_class_context(slice_ty) { - Ok(context) => Ok(Type::KnownInstance(KnownInstanceType::SubscriptedProtocol( - context, - ))), - Err(LegacyGenericContextError::InvalidArgument(argument_ty)) => { - Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::InvalidLegacyGenericArgument { - origin: LegacyGenericOrigin::Protocol, - argument_ty, - }, - )) - } - Err(LegacyGenericContextError::DuplicateTypevar(typevar_name)) => { - Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::DuplicateTypevar { - origin: LegacyGenericOrigin::Protocol, - typevar_name, - }, - )) - } - Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked) => { - Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::TypeVarTupleNotUnpacked { - origin: LegacyGenericOrigin::Protocol, - }, - )) - } - Err( - error @ (LegacyGenericContextError::NotYetSupported - | LegacyGenericContextError::VariadicTupleArguments), - ) => Ok(error.into_type()), - } - } - Type::SpecialForm(SpecialFormType::Concatenate) => { - // TODO: Add proper support for `Concatenate` - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - db, - self.typevar_binding_context, - &mut variables, - ); - let generic_context = GenericContext::from_typevar_instances(db, variables); - Ok(Type::Dynamic(DynamicType::UnknownGeneric(generic_context))) - } - _ => value_ty.subscript(self.db(), slice_ty, expr_context), - }; - - subscript_result.unwrap_or_else(|e| { - e.report_diagnostics(&self.context, subscript); - e.result_type() - }) - } - - fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { - enum SliceArg<'db> { - Arg(Type<'db>), - Unsupported, - } - - let ast::ExprSlice { - range: _, - node_index: _, - lower, - upper, - step, - } = slice; - - let ty_lower = self.infer_optional_expression(lower.as_deref(), TypeContext::default()); - let ty_upper = self.infer_optional_expression(upper.as_deref(), TypeContext::default()); - let ty_step = self.infer_optional_expression(step.as_deref(), TypeContext::default()); - - let type_to_slice_argument = |ty: Option>| match ty { - Some(ty @ Type::LiteralValue(literal)) if literal.is_int() || literal.is_bool() => { - SliceArg::Arg(ty) - } - Some(ty @ Type::NominalInstance(instance)) - if instance.has_known_class(self.db(), KnownClass::NoneType) => - { - SliceArg::Arg(ty) - } - None => SliceArg::Arg(Type::none(self.db())), - _ => SliceArg::Unsupported, - }; - - match ( - type_to_slice_argument(ty_lower), - type_to_slice_argument(ty_upper), - type_to_slice_argument(ty_step), - ) { - (SliceArg::Arg(lower), SliceArg::Arg(upper), SliceArg::Arg(step)) => { - KnownClass::Slice.to_specialized_instance(self.db(), &[lower, upper, step]) - } - _ => KnownClass::Slice.to_instance(self.db()), - } - } - fn infer_type_parameters(&mut self, type_parameters: &ast::TypeParams) { let ast::TypeParams { range: _, diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs new file mode 100644 index 0000000000000..acdc9da74097f --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -0,0 +1,981 @@ +use itertools::{EitherOrBoth, Itertools}; +use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; +use ruff_db::parsed::parsed_module; +use ruff_python_ast::{self as ast, ExprContext}; +use ruff_text_size::Ranged; + +use super::TypeInferenceBuilder; +use crate::place::{DefinedPlace, Definedness, Place}; +use crate::semantic_index::SemanticIndex; +use crate::semantic_index::definition::Definition; +use crate::semantic_index::place::{PlaceExpr, PlaceExprRef}; +use crate::semantic_index::scope::FileScopeId; +use crate::types::call::bind::CallableDescription; +use crate::types::constraints::ConstraintSetBuilder; +use crate::types::diagnostic::{ + INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, + report_invalid_arguments_to_annotated, +}; +use crate::types::generics::{GenericContext, InferableTypeVars, bind_typevar}; +use crate::types::special_form::AliasSpec; +use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; +use crate::types::tuple::{Tuple, TupleType}; +use crate::types::{ + BoundTypeVarInstance, CallableType, DynamicType, InternedType, KnownClass, KnownInstanceType, + Parameters, SpecialFormType, StaticClassLiteral, Type, TypeAliasType, TypeContext, + TypeVarBoundOrConstraints, UnionType, UnionTypeInstance, any_over_type, todo_type, +}; +use crate::{Db, FxOrderSet}; + +impl<'db> TypeInferenceBuilder<'db, '_> { + pub(super) fn infer_subscript_expression( + &mut self, + subscript: &ast::ExprSubscript, + ) -> Type<'db> { + let ast::ExprSubscript { + value, + slice, + range: _, + node_index: _, + ctx, + } = subscript; + + match ctx { + ExprContext::Load => self.infer_subscript_load(subscript), + ExprContext::Store => { + let value_ty = self.infer_expression(value, TypeContext::default()); + let slice_ty = self.infer_expression(slice, TypeContext::default()); + self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); + Type::Never + } + ExprContext::Del => { + let value_ty = self.infer_expression(value, TypeContext::default()); + let slice_ty = self.infer_expression(slice, TypeContext::default()); + self.validate_subscript_deletion(subscript, value_ty, slice_ty); + Type::Never + } + ExprContext::Invalid => { + let value_ty = self.infer_expression(value, TypeContext::default()); + let slice_ty = self.infer_expression(slice, TypeContext::default()); + self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); + Type::unknown() + } + } + } + + pub(super) fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); + + // If we have an implicit type alias like `MyList = list[T]`, and if `MyList` is being + // used in another implicit type alias like `Numbers = MyList[int]`, then we infer the + // right hand side as a value expression, and need to handle the specialization here. + if value_ty.is_generic_alias() { + return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); + } + + self.infer_subscript_load_impl(value_ty, subscript) + } + + pub(super) fn infer_subscript_load_impl( + &mut self, + value_ty: Type<'db>, + subscript: &ast::ExprSubscript, + ) -> Type<'db> { + let db = self.db(); + + let ast::ExprSubscript { + range: _, + node_index: _, + value: _, + slice, + ctx, + } = subscript; + + let mut constraint_keys = vec![]; + + // If `value` is a valid reference, we attempt type narrowing by assignment. + if !value_ty.is_unknown() { + if let Some(expr) = PlaceExpr::try_from_expr(subscript) { + let (place, keys) = self.infer_place_load( + PlaceExprRef::from(&expr), + ast::ExprRef::Subscript(subscript), + ); + constraint_keys.extend(keys); + if let Place::Defined(DefinedPlace { + ty, + definedness: Definedness::AlwaysDefined, + .. + }) = place.place + { + // Even if we can obtain the subscript type based on the assignments, we still perform default type inference + // (to store the expression type and to report errors). + let slice_ty = self.infer_expression(slice, TypeContext::default()); + self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); + return ty; + } + } + } + + let tuple_generic_alias = |db: &'db dyn Db, tuple: Option>| { + let tuple = tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown())); + Type::from(tuple.to_class_type(db)) + }; + + match value_ty { + Type::ClassLiteral(class) => { + // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the + // subscript inference logic and treat this as an explicit specialization. + // TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return + // this callable as the `__class_getitem__` method on `type`. That probably requires + // updating all of the subscript logic below to use custom callables for all of the _other_ + // special cases, too. + if class.is_tuple(db) { + return tuple_generic_alias(db, self.infer_tuple_type_expression(subscript)); + } else if class.is_known(db, KnownClass::Type) { + let argument_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + InternedType::new(db, argument_ty), + )); + } + + if let Some(generic_context) = class.generic_context(db) + && let Some(class) = class.as_static() + { + return self.infer_explicit_class_specialization( + subscript, + value_ty, + class, + generic_context, + ); + } + } + Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695( + _, + ))) => { + let slice_ty = self.infer_expression(slice, TypeContext::default()); + let mut variables = FxOrderSet::default(); + slice_ty.bind_and_find_all_legacy_typevars( + db, + self.typevar_binding_context, + &mut variables, + ); + let generic_context = GenericContext::from_typevar_instances(db, variables); + return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); + } + Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { + if let Some(generic_context) = type_alias.generic_context(db) { + return self.infer_explicit_type_alias_type_specialization( + subscript, + value_ty, + type_alias, + generic_context, + ); + } + } + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Tuple => { + return tuple_generic_alias(db, self.infer_tuple_type_expression(subscript)); + } + SpecialFormType::Literal => match self.infer_literal_parameter_type(slice) { + Ok(result) => { + return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( + db, result, + ))); + } + Err(nodes) => { + for node in nodes { + let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) + else { + continue; + }; + builder.into_diagnostic( + "Type arguments for `Literal` must be `None`, \ + a literal value (int, bool, str, or bytes), or an enum member", + ); + } + return Type::unknown(); + } + }, + SpecialFormType::Annotated => { + let ast::Expr::Tuple(ast::ExprTuple { + elts: ref arguments, + .. + }) = **slice + else { + report_invalid_arguments_to_annotated(&self.context, subscript); + + return self.infer_expression(slice, TypeContext::default()); + }; + + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); + } + + let [type_expr, metadata @ ..] = &arguments[..] else { + for argument in arguments { + self.infer_expression(argument, TypeContext::default()); + } + self.store_expression_type(slice, Type::unknown()); + return Type::unknown(); + }; + + for element in metadata { + self.infer_expression(element, TypeContext::default()); + } + + let ty = self.infer_type_expression(type_expr); + + return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( + db, ty, + ))); + } + SpecialFormType::Optional => { + if matches!(**slice, ast::Expr::Tuple(_)) + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "`typing.Optional` requires exactly one argument" + )); + } + + let ty = self.infer_type_expression(slice); + + // `Optional[None]` is equivalent to `None`: + if ty.is_none(db) { + return ty; + } + + return Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + None, + Ok(UnionType::from_two_elements(db, ty, Type::none(db))), + ), + )); + } + SpecialFormType::Union => match **slice { + ast::Expr::Tuple(ref tuple) => { + let mut elements = tuple + .elts + .iter() + .map(|elt| self.infer_type_expression(elt)) + .peekable(); + + let is_empty = elements.peek().is_none(); + let union_type = Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + None, + Ok(UnionType::from_elements(db, elements)), + ), + )); + + if is_empty + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic( + "`typing.Union` requires at least one type argument", + ); + } + + return union_type; + } + _ => { + return self.infer_expression(slice, TypeContext::default()); + } + }, + SpecialFormType::Type => { + // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` + let argument_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + InternedType::new(db, argument_ty), + )); + } + SpecialFormType::Callable => { + let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { + &*tuple.elts + } else { + std::slice::from_ref(&*subscript.slice) + }; + + // TODO: Remove this once we support Concatenate properly. This is necessary + // to avoid a lot of false positives downstream, because we can't represent the typevar- + // specialized `Callable` types yet. + if let [first_arg, second_arg] = arguments + && first_arg.is_subscript_expr() + { + let first_arg_ty = self.infer_expression(first_arg, TypeContext::default()); + if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) = + first_arg_ty + { + let mut variables = + generic_context.variables(db).collect::>(); + + let return_ty = + self.infer_expression(second_arg, TypeContext::default()); + return_ty.bind_and_find_all_legacy_typevars( + db, + self.typevar_binding_context, + &mut variables, + ); + + let generic_context = + GenericContext::from_typevar_instances(db, variables); + return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); + } + + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "The first argument to `Callable` must be either a list of types, \ + ParamSpec, Concatenate, or `...`", + )); + } + return Type::KnownInstance(KnownInstanceType::Callable( + CallableType::unknown(db), + )); + } + + let callable = self + .infer_callable_type(subscript) + .as_callable() + .expect("always returns Type::Callable"); + + return Type::KnownInstance(KnownInstanceType::Callable(callable)); + } + SpecialFormType::LegacyStdlibAlias(alias) => { + let AliasSpec { + class, + expected_argument_number, + } = alias.alias_spec(); + + let args = if let ast::Expr::Tuple(t) = &**slice { + &*t.elts + } else { + std::slice::from_ref(&**slice) + }; + + if args.len() != expected_argument_number + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + let noun = if expected_argument_number == 1 { + "argument" + } else { + "arguments" + }; + builder.into_diagnostic(format_args!( + "`typing.{name}` requires exactly \ + {expected_argument_number} {noun}, got {got}", + name = special_form.name(), + got = args.len() + )); + } + + let arg_types: Vec<_> = args + .iter() + .map(|arg| self.infer_type_expression(arg)) + .collect(); + + return class + .to_specialized_class_type(db, arg_types) + .map(Type::from) + .unwrap_or_else(Type::unknown); + } + _ => {} + }, + + Type::KnownInstance( + KnownInstanceType::UnionType(_) + | KnownInstanceType::Annotated(_) + | KnownInstanceType::Callable(_) + | KnownInstanceType::TypeGenericAlias(_), + ) => { + return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); + } + Type::Dynamic(DynamicType::Unknown) => { + let slice_ty = self.infer_expression(slice, TypeContext::default()); + let mut variables = FxOrderSet::default(); + slice_ty.bind_and_find_all_legacy_typevars( + db, + self.typevar_binding_context, + &mut variables, + ); + let generic_context = GenericContext::from_typevar_instances(db, variables); + return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); + } + _ => {} + } + + let slice_ty = self.infer_expression(slice, TypeContext::default()); + let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); + self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys) + } + + pub(super) fn infer_explicit_class_specialization( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + generic_class: StaticClassLiteral<'db>, + generic_context: GenericContext<'db>, + ) -> Type<'db> { + let db = self.db(); + let specialize = &|types: &[Option>]| { + Type::from(generic_class.apply_specialization(db, |_| { + generic_context.specialize_partial(db, types.iter().copied()) + })) + }; + + self.infer_explicit_callable_specialization( + subscript, + value_ty, + generic_context, + specialize, + ) + } + + pub(super) fn infer_explicit_type_alias_type_specialization( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + generic_type_alias: TypeAliasType<'db>, + generic_context: GenericContext<'db>, + ) -> Type<'db> { + let db = self.db(); + let specialize = &|types: &[Option>]| { + let type_alias = generic_type_alias.apply_specialization(db, |_| { + generic_context.specialize_partial(db, types.iter().copied()) + }); + + Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) + }; + + self.infer_explicit_callable_specialization( + subscript, + value_ty, + generic_context, + specialize, + ) + } + + pub(super) fn infer_explicit_callable_specialization( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + generic_context: GenericContext<'db>, + specialize: &dyn Fn(&[Option>]) -> Type<'db>, + ) -> Type<'db> { + enum ExplicitSpecializationError { + InvalidParamSpec, + UnsatisfiedBound, + UnsatisfiedConstraints, + /// These two errors override the errors above, causing all specializations to be `Unknown`. + MissingTypeVars, + TooManyArguments, + /// This error overrides the errors above, causing the type itself to be `Unknown`. + NonGeneric, + } + + fn add_typevar_definition<'db>( + db: &'db dyn Db, + diagnostic: &mut Diagnostic, + typevar: BoundTypeVarInstance<'db>, + ) { + let Some(definition) = typevar.typevar(db).definition(db) else { + return; + }; + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let range = definition.focus_range(db, &module).range(); + diagnostic.annotate( + Annotation::secondary(Span::from(file).with_range(range)) + .message("Type variable defined here"), + ); + } + + let db = self.db(); + let constraints = ConstraintSetBuilder::new(); + let slice_node = subscript.slice.as_ref(); + + let exactly_one_paramspec = generic_context.exactly_one_paramspec(db); + let (type_arguments, store_inferred_type_arguments) = match slice_node { + ast::Expr::Tuple(tuple) => { + if exactly_one_paramspec && !tuple.elts.is_empty() { + (std::slice::from_ref(slice_node), false) + } else { + (tuple.elts.as_slice(), true) + } + } + _ => (std::slice::from_ref(slice_node), false), + }; + let mut inferred_type_arguments = Vec::with_capacity(type_arguments.len()); + + let typevars = generic_context.variables(db); + let typevars_len = typevars.len(); + + let mut specialization_types = Vec::with_capacity(typevars_len); + let mut typevar_with_defaults = 0; + let mut missing_typevars = vec![]; + let mut first_excess_type_argument_index = None; + + // Helper to get the AST node corresponding to the type argument at `index`. + let get_node = |index: usize| -> ast::AnyNodeRef<'_> { + match slice_node { + ast::Expr::Tuple(ast::ExprTuple { elts, .. }) if !exactly_one_paramspec => elts + .get(index) + .expect("type argument index should not be out of range") + .into(), + _ => slice_node.into(), + } + }; + + let mut error: Option = None; + + for (index, item) in typevars.zip_longest(type_arguments.iter()).enumerate() { + match item { + EitherOrBoth::Both(typevar, expr) => { + if typevar.default_type(db).is_some() { + typevar_with_defaults += 1; + } + + let provided_type = if typevar.is_paramspec(db) { + self.infer_paramspec_explicit_specialization_value( + expr, + exactly_one_paramspec, + ) + .unwrap_or_else(|()| { + error = Some(ExplicitSpecializationError::InvalidParamSpec); + Type::paramspec_value_callable(db, Parameters::unknown()) + }) + } else { + self.infer_type_expression(expr) + }; + + inferred_type_arguments.push(provided_type); + + // TODO consider just accepting the given specialization without checking + // against bounds/constraints, but recording the expression for deferred + // checking at end of scope. This would avoid a lot of cycles caused by eagerly + // doing assignment checks here. + match typevar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + if provided_type + .when_assignable_to( + db, + bound, + &constraints, + InferableTypeVars::None, + ) + .is_never_satisfied(db) + { + let node = get_node(index); + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Type `{}` is not assignable to upper bound `{}` \ + of type variable `{}`", + provided_type.display(db), + bound.display(db), + typevar.identity(db).display(db), + )); + add_typevar_definition(db, &mut diagnostic, typevar); + } + error = Some(ExplicitSpecializationError::UnsatisfiedBound); + specialization_types.push(Some(Type::unknown())); + } else { + specialization_types.push(Some(provided_type)); + } + } + Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { + // TODO: this is wrong, the given specialization needs to be assignable + // to _at least one_ of the individual constraints, not to the union of + // all of them. `int | str` is not a valid specialization of a typevar + // constrained to `(int, str)`. + if provided_type + .when_assignable_to( + db, + typevar_constraints.as_type(db), + &constraints, + InferableTypeVars::None, + ) + .is_never_satisfied(db) + { + let node = get_node(index); + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Type `{}` does not satisfy constraints `{}` \ + of type variable `{}`", + provided_type.display(db), + typevar_constraints + .elements(db) + .iter() + .map(|c| c.display(db)) + .format("`, `"), + typevar.identity(db).display(db), + )); + add_typevar_definition(db, &mut diagnostic, typevar); + } + error = Some(ExplicitSpecializationError::UnsatisfiedConstraints); + specialization_types.push(Some(Type::unknown())); + } else { + specialization_types.push(Some(provided_type)); + } + } + None => { + specialization_types.push(Some(provided_type)); + } + } + } + EitherOrBoth::Left(typevar) => { + if typevar.default_type(db).is_none() { + // This is an error case, so no need to push into the specialization types. + missing_typevars.push(typevar); + } else { + typevar_with_defaults += 1; + specialization_types.push(None); + } + } + EitherOrBoth::Right(expr) => { + inferred_type_arguments.push(self.infer_type_expression(expr)); + first_excess_type_argument_index.get_or_insert(index); + } + } + } + + if !missing_typevars.is_empty() { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, subscript) { + let description = CallableDescription::new(db, value_ty); + let s = if missing_typevars.len() > 1 { "s" } else { "" }; + builder.into_diagnostic(format_args!( + "No type argument{s} provided for required type variable{s} `{}`{}", + missing_typevars + .iter() + .map(|tv| tv.typevar(db).name(db)) + .format("`, `"), + if let Some(CallableDescription { kind, name }) = description { + format!(" of {kind} `{name}`") + } else { + String::new() + } + )); + } + error = Some(ExplicitSpecializationError::MissingTypeVars); + } + + if let Some(first_excess_type_argument_index) = first_excess_type_argument_index { + if let Type::GenericAlias(alias) = value_ty + && let spec = alias.specialization(db) + && spec + .types(db) + .contains(&Type::Dynamic(DynamicType::TodoTypeVarTuple)) + { + // Avoid false-positive errors when specializing a class + // that's generic over a legacy TypeVarTuple + } else if typevars_len == 0 { + // Type parameter list cannot be empty, so if we reach here, `value_ty` is not a generic type. + if let Some(builder) = self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type `{}`", + value_ty.display(db) + )); + let already_specialized = match value_ty { + Type::GenericAlias(_) => true, + Type::KnownInstance(KnownInstanceType::UnionType(union)) => union + .value_expression_types(db) + .is_ok_and(|mut tys| tys.any(|ty| ty.is_generic_alias())), + _ => false, + }; + if already_specialized { + diagnostic.annotate( + self.context + .secondary(&*subscript.value) + .message("Type is already specialized"), + ); + } + } + error = Some(ExplicitSpecializationError::NonGeneric); + } else { + let node = get_node(first_excess_type_argument_index); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) { + let description = CallableDescription::new(db, value_ty); + builder.into_diagnostic(format_args!( + "Too many type arguments{}: expected {}, got {}", + if let Some(CallableDescription { kind, name }) = description { + format!(" to {kind} `{name}`") + } else { + String::new() + }, + if typevar_with_defaults == 0 { + format!("{typevars_len}") + } else { + format!( + "between {} and {}", + typevars_len - typevar_with_defaults, + typevars_len + ) + }, + type_arguments.len(), + )); + } + error = Some(ExplicitSpecializationError::TooManyArguments); + } + } + + if store_inferred_type_arguments { + self.store_expression_type( + slice_node, + Type::heterogeneous_tuple(db, inferred_type_arguments), + ); + } + + match error { + Some(ExplicitSpecializationError::NonGeneric) => Type::unknown(), + Some( + ExplicitSpecializationError::MissingTypeVars + | ExplicitSpecializationError::TooManyArguments, + ) => { + let unknowns = generic_context + .variables(db) + .map(|typevar| { + Some(if typevar.is_paramspec(db) { + Type::paramspec_value_callable(db, Parameters::unknown()) + } else { + Type::unknown() + }) + }) + .collect::>(); + specialize(&unknowns) + } + Some( + ExplicitSpecializationError::UnsatisfiedBound + | ExplicitSpecializationError::UnsatisfiedConstraints + | ExplicitSpecializationError::InvalidParamSpec, + ) + | None => specialize(&specialization_types), + } + } + + pub(super) fn infer_subscript_expression_types( + &self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + slice_ty: Type<'db>, + expr_context: ExprContext, + ) -> Type<'db> { + let db = self.db(); + + // Special typing forms for which subscriptions are context-dependent are parsed here, + // outside of `Type::subscript`, which is a pure function that doesn't depend on the + // semantic index or any context-dependent state. + let subscript_result = match value_ty { + Type::SpecialForm(SpecialFormType::Generic) => infer_legacy_generic_subscript( + db, + self.index, + self.scope().file_scope_id(db), + self.typevar_binding_context, + slice_ty, + LegacyGenericOrigin::Generic, + KnownInstanceType::SubscriptedGeneric, + ), + Type::SpecialForm(SpecialFormType::Protocol) => infer_legacy_generic_subscript( + db, + self.index, + self.scope().file_scope_id(db), + self.typevar_binding_context, + slice_ty, + LegacyGenericOrigin::Protocol, + KnownInstanceType::SubscriptedProtocol, + ), + Type::SpecialForm(SpecialFormType::Concatenate) => { + // TODO: Add proper support for `Concatenate` + let mut variables = FxOrderSet::default(); + slice_ty.bind_and_find_all_legacy_typevars( + db, + self.typevar_binding_context, + &mut variables, + ); + let generic_context = GenericContext::from_typevar_instances(db, variables); + Ok(Type::Dynamic(DynamicType::UnknownGeneric(generic_context))) + } + _ => value_ty.subscript(db, slice_ty, expr_context), + }; + + subscript_result.unwrap_or_else(|e| { + e.report_diagnostics(&self.context, subscript); + e.result_type() + }) + } + + pub(super) fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { + enum SliceArg<'db> { + Arg(Type<'db>), + Unsupported, + } + + let db = self.db(); + + let ast::ExprSlice { + range: _, + node_index: _, + lower, + upper, + step, + } = slice; + + let ty_lower = self.infer_optional_expression(lower.as_deref(), TypeContext::default()); + let ty_upper = self.infer_optional_expression(upper.as_deref(), TypeContext::default()); + let ty_step = self.infer_optional_expression(step.as_deref(), TypeContext::default()); + + let type_to_slice_argument = |ty: Option>| match ty { + Some(ty @ Type::LiteralValue(literal)) if literal.is_int() || literal.is_bool() => { + SliceArg::Arg(ty) + } + Some(ty @ Type::NominalInstance(instance)) + if instance.has_known_class(db, KnownClass::NoneType) => + { + SliceArg::Arg(ty) + } + None => SliceArg::Arg(Type::none(db)), + _ => SliceArg::Unsupported, + }; + + match ( + type_to_slice_argument(ty_lower), + type_to_slice_argument(ty_upper), + type_to_slice_argument(ty_step), + ) { + (SliceArg::Arg(lower), SliceArg::Arg(upper), SliceArg::Arg(step)) => { + KnownClass::Slice.to_specialized_instance(db, &[lower, upper, step]) + } + _ => KnownClass::Slice.to_instance(db), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LegacyGenericContextError<'db> { + /// It's invalid to subscript `Generic` or `Protocol` with this type. + InvalidArgument(Type<'db>), + /// It's invalid to subscript `Generic` or `Protocol` with a variadic tuple type. + /// We should emit a diagnostic for this, but we don't yet. + VariadicTupleArguments, + /// It's valid to subscribe `Generic` or `Protocol` with this type, + /// but the type is not yet supported. + NotYetSupported, + /// A duplicate typevar was provided. + DuplicateTypevar(&'db str), + /// A `TypeVarTuple` was provided but not unpacked. + TypeVarTupleMustBeUnpacked, +} + +impl<'db> LegacyGenericContextError<'db> { + const fn into_type(self) -> Type<'db> { + match self { + LegacyGenericContextError::InvalidArgument(_) + | LegacyGenericContextError::VariadicTupleArguments + | LegacyGenericContextError::DuplicateTypevar(_) + | LegacyGenericContextError::TypeVarTupleMustBeUnpacked => Type::unknown(), + LegacyGenericContextError::NotYetSupported => { + todo_type!("ParamSpecs and TypeVarTuples") + } + } + } +} + +/// Validate the type arguments to `Generic[...]` or `Protocol[...]`, returning +/// either the resulting [`GenericContext`] or a [`SubscriptError`]. +fn infer_legacy_generic_subscript<'db>( + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + file_scope_id: FileScopeId, + typevar_binding_context: Option>, + slice_ty: Type<'db>, + origin: LegacyGenericOrigin, + wrap_ok: impl FnOnce(GenericContext<'db>) -> KnownInstanceType<'db>, +) -> Result, SubscriptError<'db>> { + match legacy_generic_class_context(db, index, file_scope_id, typevar_binding_context, slice_ty) + { + Ok(context) => Ok(Type::KnownInstance(wrap_ok(context))), + Err(LegacyGenericContextError::InvalidArgument(argument_ty)) => Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::InvalidLegacyGenericArgument { + origin, + argument_ty, + }, + )), + Err(LegacyGenericContextError::DuplicateTypevar(typevar_name)) => Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::DuplicateTypevar { + origin, + typevar_name, + }, + )), + Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked) => Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::TypeVarTupleNotUnpacked { origin }, + )), + Err( + error @ (LegacyGenericContextError::NotYetSupported + | LegacyGenericContextError::VariadicTupleArguments), + ) => Ok(error.into_type()), + } +} + +/// Parse the type arguments to `Generic[...]` or `Protocol[...]` and validate +/// that each argument is a type variable. +fn legacy_generic_class_context<'db>( + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + file_scope_id: FileScopeId, + typevar_binding_context: Option>, + typevars: Type<'db>, +) -> Result, LegacyGenericContextError<'db>> { + let typevars_class_tuple_spec = typevars.exact_tuple_instance_spec(db); + + let typevars = if let Some(tuple_spec) = typevars_class_tuple_spec.as_deref() { + match tuple_spec { + Tuple::Fixed(typevars) => typevars.elements_slice(), + Tuple::Variable(_) => { + return Err(LegacyGenericContextError::VariadicTupleArguments); + } + } + } else { + std::slice::from_ref(&typevars) + }; + + let mut validated_typevars = FxOrderSet::default(); + for ty in typevars { + let argument_ty = *ty; + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = argument_ty { + let bound = bind_typevar(db, index, file_scope_id, typevar_binding_context, typevar) + .ok_or(LegacyGenericContextError::InvalidArgument(argument_ty))?; + if !validated_typevars.insert(bound) { + return Err(LegacyGenericContextError::DuplicateTypevar( + typevar.name(db), + )); + } + } else if let Type::NominalInstance(instance) = argument_ty + && instance.has_known_class(db, KnownClass::TypeVarTuple) + { + return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked); + } else if any_over_type(db, argument_ty, true, |inner_ty| match inner_ty { + Type::Dynamic(DynamicType::TodoUnpack | DynamicType::TodoStarredExpression) => true, + Type::NominalInstance(nominal) => nominal.has_known_class(db, KnownClass::TypeVarTuple), + _ => false, + }) { + return Err(LegacyGenericContextError::NotYetSupported); + } else { + return Err(LegacyGenericContextError::InvalidArgument(argument_ty)); + } + } + Ok(GenericContext::from_typevar_instances( + db, + validated_typevars, + )) +} From f18d2ca1183db75e5ead165a448ebc0060cb1b17 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Mar 2026 13:21:19 +0000 Subject: [PATCH 163/261] Improvements to CLAUDE.md (#23633) --- CLAUDE.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bd099276d90f2..a42126a8653ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,13 +78,16 @@ When working on ty, PR titles should start with `[ty]` and be tagged with the `t ## Development Guidelines - All changes must be tested. If you're not testing your changes, you're not done. +- Look to see if your tests could go in an existing file before adding a new file for your tests. - Get your tests to pass. If you didn't run the tests, your code does not work. - Follow existing code style. Check neighboring files for patterns. -- Always run `uvx prek run -a` at the end of a task. +- Rust imports should always go at the top of the file, never locally in functions. +- Always run `uvx prek run -a` at the end of a task, after every rebase, after addressing any review comment, and before pushing any code. - Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first. -- Avoid falling back to patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system. -- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability. -- If you *have* to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. +- Try hard to avoid patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls. +- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability. At the end of a task, always check your work to see if you missed opportunities to use `let` chains. +- If you *have* to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. But if a lint is complaining about unused/dead code, it's usually best to just delete the unused code. - Use comments purposefully. Don't use comments to narrate code, but do use them to explain invariants and why something unusual was done a particular way. +- When adding new ty checks, it's important to make error messages concise. Think about how an error message would look on a narrow terminal screen. Sometimes more detail can be provided in subdiagnostics or secondary annotations, but it's also important to make sure that the diagnostic is understandable if the user has passed `--output-format=concise`. - **Salsa incrementality (ty):** Any method that accesses `.node()` must be `#[salsa::tracked]`, or it will break incrementality. Prefer higher-level semantic APIs over raw AST access. - Run `cargo dev generate-all` after changing configuration options, CLI arguments, lint rules, or environment variable definitions, as these changes require regeneration of schemas, docs, and CLI references. From 7f434ca3a37f299e69ff422190d27e9dce28c413 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Mon, 2 Mar 2026 05:30:18 -0800 Subject: [PATCH 164/261] [ty] make `StaticClassLiteral::explicit_bases` converge better in cycles (#23601) --- .../resources/mdtest/cycle.md | 70 +++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 39 ++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 8f5816fdc4953..695639f38de79 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -155,3 +155,73 @@ class Cyclic: # revealed: Unknown | str | dict[Unknown, Unknown] | dict[Unknown | str, Unknown | str] reveal_type(Cyclic("").data) ``` + +## Decorator defined on a base class with constrained typevars, accessed from a subclass with decorated generic parameters + +This example was minimized from +[a real issue in `robotframework`](https://github.com/astral-sh/ty/issues/2637#issuecomment-3807037935). +It created +[a complicated cycle with multiple cycle heads](https://gist.github.com/oconnor663/c996ed2cc97d172dd4b9a8d8207dc7ac), +which also involved +[a tricky Salsa behavior that comes up when a query oscillates between being a cycle head and not being one](https://gist.github.com/oconnor663/c2a7662e3d88048b691754da957121d1). + +`entry.py`: + +```py +from derived import Derived + +Derived.decorate +# revealed: bound method .decorate[T](item_class: type[T]) -> type[T] +reveal_type(Derived.decorate) +``` + +`derived.py`: + +```py +from ty_extensions import reveal_mro +import bases + +class Derived(bases.GenericBase["Foo", "Bar"]): ... + +@Derived.decorate +class Foo(bases.Foo): ... + +# revealed: +reveal_type(Foo) +# revealed: (, , ) +reveal_mro(Foo) + +@Derived.decorate +class Bar(bases.Bar): ... + +# revealed: +reveal_type(Bar) +# revealed: (, , ) +reveal_mro(Bar) +``` + +`bases.py`: + +```py +from typing import Generic, TypeVar, Type +from ty_extensions import reveal_mro + +T = TypeVar("T") +B1 = TypeVar("B1", bound="Foo") +B2 = TypeVar("B2", bound="Bar") + +class GenericBase(Generic[B1, B2]): + @classmethod + def decorate(cls, item_class: Type[T]) -> Type[T]: + return item_class + +# revealed: +reveal_type(GenericBase) +# revealed: (, typing.Generic, ) +reveal_mro(GenericBase) +# revealed: (, typing.Generic, ) +reveal_mro(GenericBase["Foo", "Bar"]) + +class Foo: ... +class Bar: ... +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 6532df131e769..9df5d4842c9b7 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -127,6 +127,43 @@ fn try_metaclass_cycle_initial<'db>( }) } +fn explicit_bases_cycle_initial<'db>( + db: &'db dyn Db, + id: salsa::Id, + literal: StaticClassLiteral<'db>, +) -> Box<[Type<'db>]> { + let module = parsed_module(db, literal.file(db)).load(db); + let class_stmt = literal.node(db, &module); + // Try to produce a list of `Divergent` types of the right length. However, if one or more of + // the bases is a starred expression, we don't know how many entries that will eventually + // expand to. + vec![Type::divergent(id); class_stmt.bases().len()].into_boxed_slice() +} + +fn explicit_bases_cycle_fn<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + previous: &[Type<'db>], + current: Box<[Type<'db>]>, + _literal: StaticClassLiteral<'db>, +) -> Box<[Type<'db>]> { + if previous.len() == current.len() { + // As long as the length of bases hasn't changed, use the same "monotonic widening" + // strategy that we use with most types, to avoid oscillations. + current + .iter() + .zip(previous.iter()) + .map(|(curr, prev)| curr.cycle_normalized(db, *prev, cycle)) + .collect() + } else { + // The length of bases has changed, presumably because we expanded a starred expression. We + // don't do "monotonic widening" here, because we don't want to make assumptions about + // which previous entries correspond to which current ones. An oscillation here would be + // unfortunate, but maybe only pathological programs can trigger such a thing. + current + } +} + #[expect(clippy::unnecessary_wraps)] fn dynamic_class_try_mro_cycle_initial<'db>( db: &'db dyn Db, @@ -2475,7 +2512,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// Were this not a salsa query, then the calling query /// would depend on the class's AST and rerun for every change in that file. - #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(deref), cycle_initial=explicit_bases_cycle_initial, cycle_fn=explicit_bases_cycle_fn, heap_size=ruff_memory_usage::heap_size)] pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { tracing::trace!( "StaticClassLiteral::explicit_bases_query: {}", From 7cd738a0295a00a85af18a46483f45263ad98ecf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:04:24 -0500 Subject: [PATCH 165/261] Update prek dependencies (#23661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | Pending | |---|---|---|---|---| | [astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit) | repository | patch | `v0.15.1` → `v0.15.2` | `v0.15.4` (+1) | | [crate-ci/typos](https://redirect.github.com/crate-ci/typos) | repository | patch | `v1.43.4` → `v1.43.5` | `v1.44.0` | | [python-jsonschema/check-jsonschema](https://redirect.github.com/python-jsonschema/check-jsonschema) | repository | patch | `0.36.1` → `0.36.2` | `0.37.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://redirect.github.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit) ### [`v0.15.2`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.15.2) [Compare Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.15.1...v0.15.2) See:
crate-ci/typos (crate-ci/typos) ### [`v1.43.5`](https://redirect.github.com/crate-ci/typos/blob/HEAD/CHANGELOG.md#014---2019-11-03) [Compare Source](https://redirect.github.com/crate-ci/typos/compare/v1.43.4...v1.43.5) ##### Bug Fixes - Ignore numbers as identifiers ([a00831c8](https://redirect.github.com/crate-ci/typos/commit/a00831c847b7efd81be520ea9b5d02f70555351f)) - Improve the organization of --help ([a48a457c](https://redirect.github.com/crate-ci/typos/commit/a48a457cc3ca817850118e2a2fb8b20fecdd40b8)) ##### Features - Dump files, identifiers, and words ([ce365ae1](https://redirect.github.com/crate-ci/typos/commit/ce365ae12e12fddfb6fc42a7f1e5ea71834d6051), closes [#​41](https://redirect.github.com/crate-ci/typos/issues/41)) - Give control over allowed identifier characters for leading vs rest ([107308a6](https://redirect.github.com/crate-ci/typos/commit/107308a655a425eb593bf5e4928572c16e6a9bdd)) ##### Performance - Use standard identifier rules to avoid doing umber checks ([107308a6](https://redirect.github.com/crate-ci/typos/commit/107308a655a425eb593bf5e4928572c16e6a9bdd)) - Only do hex check if digits are in identifiers ([68cd36d0](https://redirect.github.com/crate-ci/typos/commit/68cd36d0de90226dbc9d31c2ce6d8bf6b69adb5c)) [Unreleased]: https://redirect.github.com/crate-ci/typos/compare/v1.43.5...HEAD [1.43.5]: https://redirect.github.com/crate-ci/typos/compare/v1.43.4...v1.43.5 [1.43.4]: https://redirect.github.com/crate-ci/typos/compare/v1.43.3...v1.43.4 [1.43.3]: https://redirect.github.com/crate-ci/typos/compare/v1.43.2...v1.43.3 [1.43.2]: https://redirect.github.com/crate-ci/typos/compare/v1.43.1...v1.43.2 [1.43.1]: https://redirect.github.com/crate-ci/typos/compare/v1.43.0...v1.43.1 [1.43.0]: https://redirect.github.com/crate-ci/typos/compare/v1.42.3...v1.43.0 [1.42.3]: https://redirect.github.com/crate-ci/typos/compare/v1.42.2...v1.42.3 [1.42.2]: https://redirect.github.com/crate-ci/typos/compare/v1.42.1...v1.42.2 [1.42.1]: https://redirect.github.com/crate-ci/typos/compare/v1.42.0...v1.42.1 [1.42.0]: https://redirect.github.com/crate-ci/typos/compare/v1.41.0...v1.42.0 [1.41.0]: https://redirect.github.com/crate-ci/typos/compare/v1.40.1...v1.41.0 [1.40.1]: https://redirect.github.com/crate-ci/typos/compare/v1.40.0...v1.40.1 [1.40.0]: https://redirect.github.com/crate-ci/typos/compare/v1.39.2...v1.40.0 [1.39.2]: https://redirect.github.com/crate-ci/typos/compare/v1.39.1...v1.39.2 [1.39.1]: https://redirect.github.com/crate-ci/typos/compare/v1.39.0...v1.39.1 [1.39.0]: https://redirect.github.com/crate-ci/typos/compare/v1.38.1...v1.39.0 [1.38.1]: https://redirect.github.com/crate-ci/typos/compare/v1.38.0...v1.38.1 [1.38.0]: https://redirect.github.com/crate-ci/typos/compare/v1.37.3...v1.38.0 [1.37.3]: https://redirect.github.com/crate-ci/typos/compare/v1.37.2...v1.37.3 [1.37.2]: https://redirect.github.com/crate-ci/typos/compare/v1.37.1...v1.37.2 [1.37.1]: https://redirect.github.com/crate-ci/typos/compare/v1.37.0...v1.37.1 [1.37.0]: https://redirect.github.com/crate-ci/typos/compare/v1.36.3...v1.37.0 [1.36.3]: https://redirect.github.com/crate-ci/typos/compare/v1.36.2...v1.36.3 [1.36.2]: https://redirect.github.com/crate-ci/typos/compare/v1.36.1...v1.36.2 [1.36.1]: https://redirect.github.com/crate-ci/typos/compare/v1.36.0...v1.36.1 [1.36.0]: https://redirect.github.com/crate-ci/typos/compare/v1.35.8...v1.36.0 [1.35.8]: https://redirect.github.com/crate-ci/typos/compare/v1.35.7...v1.35.8 [1.35.7]: https://redirect.github.com/crate-ci/typos/compare/v1.35.6...v1.35.7 [1.35.6]: https://redirect.github.com/crate-ci/typos/compare/v1.35.5...v1.35.6 [1.35.5]: https://redirect.github.com/crate-ci/typos/compare/v1.35.4...v1.35.5 [1.35.4]: https://redirect.github.com/crate-ci/typos/compare/v1.35.3...v1.35.4 [1.35.3]: https://redirect.github.com/crate-ci/typos/compare/v1.35.2...v1.35.3 [1.35.2]: https://redirect.github.com/crate-ci/typos/compare/v1.35.1...v1.35.2 [1.35.1]: https://redirect.github.com/crate-ci/typos/compare/v1.35.0...v1.35.1 [1.35.0]: https://redirect.github.com/crate-ci/typos/compare/v1.34.0...v1.35.0 [1.34.0]: https://redirect.github.com/crate-ci/typos/compare/v1.33.1...v1.34.0 [1.33.1]: https://redirect.github.com/crate-ci/typos/compare/v1.33.0...v1.33.1 [1.33.0]: https://redirect.github.com/crate-ci/typos/compare/v1.32.0...v1.33.0 [1.32.0]: https://redirect.github.com/crate-ci/typos/compare/v1.31.2...v1.32.0 [1.31.2]: https://redirect.github.com/crate-ci/typos/compare/v1.31.1...v1.31.2 [1.31.1]: https://redirect.github.com/crate-ci/typos/compare/v1.31.0...v1.31.1 [1.31.0]: https://redirect.github.com/crate-ci/typos/compare/v1.30.3...v1.31.0 [1.30.3]: https://redirect.github.com/crate-ci/typos/compare/v1.30.2...v1.30.3 [1.30.2]: https://redirect.github.com/crate-ci/typos/compare/v1.30.1...v1.30.2 [1.30.1]: https://redirect.github.com/crate-ci/typos/compare/v1.30.0...v1.30.1 [1.30.0]: https://redirect.github.com/crate-ci/typos/compare/v1.29.10...v1.30.0 [1.29.10]: https://redirect.github.com/crate-ci/typos/compare/v1.29.9...v1.29.10 [1.29.9]: https://redirect.github.com/crate-ci/typos/compare/v1.29.8...v1.29.9 [1.29.8]: https://redirect.github.com/crate-ci/typos/compare/v1.29.7...v1.29.8 [1.29.7]: https://redirect.github.com/crate-ci/typos/compare/v1.29.6...v1.29.7 [1.29.6]: https://redirect.github.com/crate-ci/typos/compare/v1.29.5...v1.29.6 [1.29.5]: https://redirect.github.com/crate-ci/typos/compare/v1.29.4...v1.29.5 [1.29.4]: https://redirect.github.com/crate-ci/typos/compare/v1.29.3...v1.29.4 [1.29.3]: https://redirect.github.com/crate-ci/typos/compare/v1.29.2...v1.29.3 [1.29.2]: https://redirect.github.com/crate-ci/typos/compare/v1.29.1...v1.29.2 [1.29.1]: https://redirect.github.com/crate-ci/typos/compare/v1.29.0...v1.29.1 [1.29.0]: https://redirect.github.com/crate-ci/typos/compare/v1.28.4...v1.29.0 [1.28.4]: https://redirect.github.com/crate-ci/typos/compare/v1.28.3...v1.28.4 [1.28.3]: https://redirect.github.com/crate-ci/typos/compare/v1.28.2...v1.28.3 [1.28.2]: https://redirect.github.com/crate-ci/typos/compare/v1.28.1...v1.28.2 [1.28.1]: https://redirect.github.com/crate-ci/typos/compare/v1.28.0...v1.28.1 [1.28.0]: https://redirect.github.com/crate-ci/typos/compare/v1.27.3...v1.28.0 [1.27.3]: https://redirect.github.com/crate-ci/typos/compare/v1.27.2...v1.27.3 [1.27.2]: https://redirect.github.com/crate-ci/typos/compare/v1.27.1...v1.27.2 [1.27.1]: https://redirect.github.com/crate-ci/typos/compare/v1.27.0...v1.27.1 [1.27.0]: https://redirect.github.com/crate-ci/typos/compare/v1.26.8...v1.27.0 [1.26.8]: https://redirect.github.com/crate-ci/typos/compare/v1.26.7...v1.26.8 [1.26.7]: https://redirect.github.com/crate-ci/typos/compare/v1.26.6...v1.26.7 [1.26.6]: https://redirect.github.com/crate-ci/typos/compare/v1.26.5...v1.26.6 [1.26.5]: https://redirect.github.com/crate-ci/typos/compare/v1.26.4...v1.26.5 [1.26.4]: https://redirect.github.com/crate-ci/typos/compare/v1.26.3...v1.26.4 [1.26.3]: https://redirect.github.com/crate-ci/typos/compare/v1.26.2...v1.26.3 [1.26.2]: https://redirect.github.com/crate-ci/typos/compare/v1.26.1...v1.26.2 [1.26.1]: https://redirect.github.com/crate-ci/typos/compare/v1.26.0...v1.26.1 [1.26.0]: https://redirect.github.com/crate-ci/typos/compare/v1.25.0...v1.26.0 [1.25.0]: https://redirect.github.com/crate-ci/typos/compare/v1.24.6...v1.25.0 [1.24.6]: https://redirect.github.com/crate-ci/typos/compare/v1.24.5...v1.24.6 [1.24.5]: https://redirect.github.com/crate-ci/typos/compare/v1.24.4...v1.24.5 [1.24.4]: https://redirect.github.com/crate-ci/typos/compare/v1.24.3...v1.24.4 [1.24.3]: https://redirect.github.com/crate-ci/typos/compare/v1.24.2...v1.24.3 [1.24.2]: https://redirect.github.com/crate-ci/typos/compare/v1.24.1...v1.24.2 [1.24.1]: https://redirect.github.com/crate-ci/typos/compare/v1.24.0...v1.24.1 [1.24.0]: https://redirect.github.com/crate-ci/typos/compare/v1.23.7...v1.24.0 [1.23.7]: https://redirect.github.com/crate-ci/typos/compare/v1.23.6...v1.23.7 [1.23.6]: https://redirect.github.com/crate-ci/typos/compare/v1.23.5...v1.23.6 [1.23.5]: https://redirect.github.com/crate-ci/typos/compare/v1.23.4...v1.23.5 [1.23.4]: https://redirect.github.com/crate-ci/typos/compare/v1.23.3...v1.23.4 [1.23.3]: https://redirect.github.com/crate-ci/typos/compare/v1.23.2...v1.23.3 [1.23.2]: https://redirect.github.com/crate-ci/typos/compare/v1.23.1...v1.23.2 [1.23.1]: https://redirect.github.com/crate-ci/typos/compare/v1.23.0...v1.23.1 [1.23.0]: https://redirect.github.com/crate-ci/typos/compare/v1.22.9...v1.23.0 [1.22.9]: https://redirect.github.com/crate-ci/typos/compare/v1.22.8...v1.22.9 [1.22.8]: https://redirect.github.com/crate-ci/typos/compare/v1.22.7...v1.22.8 [1.22.7]: https://redirect.github.com/crate-ci/typos/compare/v1.22.6...v1.22.7 [1.22.6]: https://redirect.github.com/crate-ci/typos/compare/v1.22.5...v1.22.6 [1.22.5]: https://redirect.github.com/crate-ci/typos/compare/v1.22.4...v1.22.5 [1.22.4]: https://redirect.github.com/crate-ci/typos/compare/v1.22.3...v1.22.4 [1.22.3]: https://redirect.github.com/crate-ci/typos/compare/v1.22.2...v1.22.3 [1.22.2]: https://redirect.github.com/crate-ci/typos/compare/v1.22.1...v1.22.2 [1.22.1]: https://redirect.github.com/crate-ci/typos/compare/v1.22.0...v1.22.1 [1.22.0]: https://redirect.github.com/crate-ci/typos/compare/v1.21.0...v1.22.0 [1.21.0]: https://redirect.github.com/crate-ci/typos/compare/v1.20.10...v1.21.0 [1.20.10]: https://redirect.github.com/crate-ci/typos/compare/v1.20.9...v1.20.10 [1.20.9]: https://redirect.github.com/crate-ci/typos/compare/v1.20.8...v1.20.9 [1.20.8]: https://redirect.github.com/crate-ci/typos/compare/v1.20.7...v1.20.8 [1.20.7]: https://redirect.github.com/crate-ci/typos/compare/v1.20.6...v1.20.7 [1.20.6]: https://redirect.github.com/crate-ci/typos/compare/v1.20.5...v1.20.6 [1.20.5]: https://redirect.github.com/crate-ci/typos/compare/v1.20.4...v1.20.5 [1.20.4]: https://redirect.github.com/crate-ci/typos/compare/v1.20.3...v1.20.4 [1.20.3]: https://redirect.github.com/crate-ci/typos/compare/v1.20.2...v1.20.3 [1.20.2]: https://redirect.github.com/crate-ci/typos/compare/v1.20.1...v1.20.2 [1.20.1]: https://redirect.github.com/crate-ci/typos/compare/v1.20.0...v1.20.1 [1.20.0]: https://redirect.github.com/crate-ci/typos/compare/v1.19.0...v1.20.0 [1.19.0]: https://redirect.github.com/crate-ci/typos/compare/v1.18.2...v1.19.0 [1.18.2]: https://redirect.github.com/crate-ci/typos/compare/v1.18.1...v1.18.2 [1.18.1]: https://redirect.github.com/crate-ci/typos/compare/v1.18.0...v1.18.1 [1.18.0]: https://redirect.github.com/crate-ci/typos/compare/v1.17.2...v1.18.0 [1.17.2]: https://redirect.github.com/crate-ci/typos/compare/v1.17.1...v1.17.2 [1.17.1]: https://redirect.github.com/crate-ci/typos/compare/v1.17.0...v1.17.1 [1.17.0]: https://redirect.github.com/crate-ci/typos/compare/v1.16.26...v1.17.0 [1.16.26]: https://redirect.github.com/crate-ci/typos/compare/v1.16.25...v1.16.26 [1.16.25]: https://redirect.github.com/crate-ci/typos/compare/v1.16.24...v1.16.25 [1.16.24]: https://redirect.github.com/crate-ci/typos/compare/v1.16.23...v1.16.24 [1.16.23]: https://redirect.github.com/crate-ci/typos/compare/v1.16.22...v1.16.23 [1.16.22]: https://redirect.github.com/crate-ci/typos/compare/v1.16.21...v1.16.22 [1.16.21]: https://redirect.github.com/crate-ci/typos/compare/v1.16.20...v1.16.21 [1.16.20]: https://redirect.github.com/crate-ci/typos/compare/v1.16.19...v1.16.20 [1.16.19]: https://redirect.github.com/crate-ci/typos/compare/v1.16.18...v1.16.19 [1.16.18]: https://redirect.github.com/crate-ci/typos/compare/v1.16.17...v1.16.18 [1.16.17]: https://redirect.github.com/crate-ci/typos/compare/v1.16.16...v1.16.17 [1.16.16]: https://redirect.github.com/crate-ci/typos/compare/v1.16.15...v1.16.16 [1.16.15]: https://redirect.github.com/crate-ci/typos/compare/v1.16.14...v1.16.15 [1.16.14]: https://redirect.github.com/crate-ci/typos/compare/v1.16.13...v1.16.14 [1.16.13]: https://redirect.github.com/crate-ci/typos/compare/v1.16.12...v1.16.13 [1.16.12]: https://redirect.github.com/crate-ci/typos/compare/v1.16.11...v1.16.12 [1.16.11]: https://redirect.github.com/crate-ci/typos/compare/v1.16.10...v1.16.11 [1.16.10]: https://redirect.github.com/crate-ci/typos/compare/v1.16.9...v1.16.10 [1.16.9]: https://redirect.github.com/crate-ci/typos/compare/v1.16.8...v1.16.9 [1.16.8]: https://redirect.github.com/crate-ci/typos/compare/v1.16.7...v1.16.8 [1.16.7]: https://redirect.github.com/crate-ci/typos/compare/v1.16.6...v1.16.7 [1.16.6]: https://redirect.github.com/crate-ci/typos/compare/v1.16.5...v1.16.6 [1.16.5]: https://redirect.github.com/crate-ci/typos/compare/v1.16.4...v1.16.5 [1.16.4]: https://redirect.github.com/crate-ci/typos/compare/v1.16.3...v1.16.4 [1.16.3]: https://redirect.github.com/crate-ci/typos/compare/v1.16.2...v1.16.3 [1.16.2]: https://redirect.github.com/crate-ci/typos/compare/v1.16.1...v1.16.2 [1.16.1]: https://redirect.github.com/crate-ci/typos/compare/v1.16.0...v1.16.1 [1.16.0]: https://redirect.github.com/crate-ci/typos/compare/v1.15.10...v1.16.0 [1.15.10]: https://redirect.github.com/crate-ci/typos/compare/v1.15.9...v1.15.10 [1.15.9]: https://redirect.github.com/crate-ci/typos/compare/v1.15.8...v1.15.9 [1.15.8]: https://redirect.github.com/crate-ci/typos/compare/v1.15.7...v1.15.8 [1.15.7]: https://redirect.github.com/crate-ci/typos/compare/v1.15.6...v1.15.7 [1.15.6]: https://redirect.github.com/crate-ci/typos/compare/v1.15.5...v1.15.6 [1.15.5]: https://redirect.github.com/crate-ci/typos/compare/v1.15.4...v1.15.5 [1.15.4]: https://redirect.github.com/crate-ci/typos/compare/v1.15.3...v1.15.4 [1.15.3]: https://redirect.github.com/crate-ci/typos/compare/v1.15.2...v1.15.3 [1.15.2]: https://redirect.github.com/crate-ci/typos/compare/v1.15.1...v1.15.2 [1.15.1]: https://redirect.github.com/crate-ci/typos/compare/v1.15.0...v1.15.1 [1.15.0]: https://redirect.github.com/crate-ci/typos/compare/v1.14.12...v1.15.0 [1.14.12]: https://redirect.github.com/crate-ci/typos/compare/v1.14.11...v1.14.12 [1.14.11]: https://redirect.github.com/crate-ci/typos/compare/v1.14.10...v1.14.11 [1.14.10]: https://redirect.github.com/crate-ci/typos/compare/v1.14.9...v1.14.10 [1.14.9]: https://redirect.github.com/crate-ci/typos/compare/v1.14.8...v1.14.9 [1.14.8]: https://redirect.github.com/crate-ci/typos/compare/v1.14.7...v1.14.8 [1.14.7]: https://redirect.github.com/crate-ci/typos/compare/v1.14.6...v1.14.7 [1.14.6]: https://redirect.github.com/crate-ci/typos/compare/v1.14.5...v1.14.6 [1.14.5]: https://redirect.github.com/crate-ci/typos/compare/v1.14.4...v1.14.5 [1.14.4]: https://redirect.github.com/crate-ci/typos/compare/v1.14.3...v1.14.4 [1.14.3]: https://redirect.github.com/crate-ci/typos/compare/v1.14.2...v1.14.3 [1.14.2]: https://redirect.github.com/crate-ci/typos/compare/v1.14.1...v1.14.2 [1.14.1]: https://redirect.github.com/crate-ci/typos/compare/v1.14.0...v1.14.1 [1.14.0]: https://redirect.github.com/crate-ci/typos/compare/v1.13.26...v1.14.0 [1.13.26]: https://redirect.github.com/crate-ci/typos/compare/v1.13.25...v1.13.26 [1.13.25]: https://redirect.github.com/crate-ci/typos/compare/v1.13.24...v1.13.25 [1.13.24]: https://redirect.github.com/crate-ci/typos/compare/v1.13.23...v1.13.24 [1.13.23]: https://redirect.github.com/crate-ci/typos/compare/v1.13.22...v1.13.23 [1.13.22]: https://redirect.github.com/crate-ci/typos/compare/v1.13.21...v1.13.22 [1.13.21]: https://redirect.github.com/crate-ci/typos/compare/v1.13.20...v1.13.21 [1.13.20]: https://redirect.github.com/crate-ci/typos/compare/v1.13.19...v1.13.20 [1.13.19]: https://redirect.github.com/crate-ci/typos/compare/v1.13.18...v1.13.19 [1.13.18]: https://redirect.github.com/crate-ci/typos/compare/v1.13.17...v1.13.18 [1.13.17]: https://redirect.github.com/crate-ci/typos/compare/v1.13.16...v1.13.17 [1.13.16]: https://redirect.github.com/crate-ci/typos/compare/v1.13.15...v1.13.16 [1.13.15]: https://redirect.github.com/crate-ci/typos/compare/v1.13.14...v1.13.15 [1.13.14]: https://redirect.github.com/crate-ci/typos/compare/v1.13.13...v1.13.14 [1.13.13]: https://redirect.github.com/crate-ci/typos/compare/v1.13.12...v1.13.13 [1.13.12]: https://redirect.github.com/crate-ci/typos/compare/v1.13.11...v1.13.12 [1.13.11]: https://redirect.github.com/crate-ci/typos/compare/v1.13.10...v1.13.11 [1.13.10]: https://redirect.github.com/crate-ci/typos/compare/v1.13.9...v1.13.10 [1.13.9]: https://redirect.github.com/crate-ci/typos/compare/v1.13.8...v1.13.9 [1.13.8]: https://redirect.github.com/crate-ci/typos/compare/v1.13.7...v1.13.8 [1.13.7]: https://redirect.github.com/crate-ci/typos/compare/v1.13.6...v1.13.7 [1.13.6]: https://redirect.github.com/crate-ci/typos/compare/v1.13.5...v1.13.6 [1.13.5]: https://redirect.github.com/crate-ci/typos/compare/v1.13.4...v1.13.5 [1.13.4]: https://redirect.github.com/crate-ci/typos/compare/v1.13.3...v1.13.4 [1.13.3]: https://redirect.github.com/crate-ci/typos/compare/v1.13.2...v1.13.3 [1.13.2]: https://redirect.github.com/crate-ci/typos/compare/v1.13.1...v1.13.2 [1.13.1]: https://redirect.github.com/crate-ci/typos/compare/v1.13.0...v1.13.1 [1.13.0]: https://redirect.github.com/crate-ci/typos/compare/v1.12.14...v1.13.0 [1.12.14]: https://redirect.github.com/crate-ci/typos/compare/v1.12.13...v1.12.14 [1.12.13]: https://redirect.github.com/crate-ci/typos/compare/v1.12.12...v1.12.13 [1.12.12]: https://redirect.github.com/crate-ci/typos/compare/v1.12.11...v1.12.12 [1.12.11]: https://redirect.github.com/crate-ci/typos/compare/v1.12.10...v1.12.11 [1.12.10]: https://redirect.github.com/crate-ci/typos/compare/v1.12.9...v1.12.10 [1.12.9]: https://redirect.github.com/crate-ci/typos/compare/v1.12.8...v1.12.9 [1.12.8]: https://redirect.github.com/crate-ci/typos/compare/v1.12.7...v1.12.8 [1.12.7]: https://redirect.github.com/crate-ci/typos/compare/v1.12.6...v1.12.7 [1.12.6]: https://redirect.github.com/crate-ci/typos/compare/v1.12.5...v1.12.6 [1.12.5]: https://redirect.github.com/crate-ci/typos/compare/v1.12.4...v1.12.5 [1.12.4]: https://redirect.github.com/crate-ci/typos/compare/v1.12.3...v1.12.4 [1.12.3]: https://redirect.github.com/crate-ci/typos/compare/v1.12.2...v1.12.3 [1.12.2]: https://redirect.github.com/crate-ci/typos/compare/v1.12.1...v1.12.2 [1.12.1]: https://redirect.github.com/crate-ci/typos/compare/v1.12.0...v1.12.1 [1.12.0]: https://redirect.github.com/crate-ci/typos/compare/v1.11.5...v1.12.0 [1.11.5]: https://redirect.github.com/crate-ci/typos/compare/v1.11.4...v1.11.5 [1.11.4]: https://redirect.github.com/crate-ci/typos/compare/v1.11.3...v1.11.4 [1.11.3]: https://redirect.github.com/crate-ci/typos/compare/v1.11.2...v1.11.3 [1.11.2]: https://redirect.github.com/crate-ci/typos/compare/v1.11.1...v1.11.2 [1.11.1]: https://redirect.github.com/crate-ci/typos/compare/v1.11.0...v1.11.1 [1.11.0]: https://redirect.github.com/crate-ci/typos/compare/v1.10.3...v1.11.0 [1.10.3]: https://redirect.github.com/crate-ci/typos/compare/v1.10.2...v1.10.3 [1.10.2]: https://redirect.github.com/crate-ci/typos/compare/v1.10.1...v1.10.2 [1.10.1]: https://redirect.github.com/crate-ci/typos/compare/v1.10.0...v1.10.1 [1.10.0]: https://redirect.github.com/crate-ci/typos/compare/v1.9.0...v1.10.0 [1.9.0]: https://redirect.github.com/crate-ci/typos/compare/v1.8.1...v1.9.0 [1.8.1]: https://redirect.github.com/crate-ci/typos/compare/v1.8.0...v1.8.1 [1.8.0]: https://redirect.github.com/crate-ci/typos/compare/v1.7.3...v1.8.0 [1.7.3]: https://redirect.github.com/crate-ci/typos/compare/v1.7.2...v1.7.3 [1.7.2]: https://redirect.github.com/crate-ci/typos/compare/v1.7.1...v1.7.2 [1.7.1]: https://redirect.github.com/crate-ci/typos/compare/v1.7.0...v1.7.1 [1.7.0]: https://redirect.github.com/crate-ci/typos/compare/v1.6.0...v1.7.0 [1.6.0]: https://redirect.github.com/crate-ci/typos/compare/v1.5.0...v1.6.0 [1.5.0]: https://redirect.github.com/crate-ci/typos/compare/v1.4.1...v1.5.0 [1.4.1]: https://redirect.github.com/crate-ci/typos/compare/v1.4.0...v1.4.1 [1.4.0]: https://redirect.github.com/crate-ci/typos/compare/v1.3.9...v1.4.0 [1.3.9]: https://redirect.github.com/crate-ci/typos/compare/v1.3.8...v1.3.9 [1.3.8]: https://redirect.github.com/crate-ci/typos/compare/v1.3.7...v1.3.8 [1.3.7]: https://redirect.github.com/crate-ci/typos/compare/v1.3.6...v1.3.7 [1.3.6]: https://redirect.github.com/crate-ci/typos/compare/v1.3.5...v1.3.6 [1.3.5]: https://redirect.github.com/crate-ci/typos/compare/v1.3.4...v1.3.5 [1.3.4]: https://redirect.github.com/crate-ci/typos/compare/v1.3.3...v1.3.4 [1.3.3]: https://redirect.github.com/crate-ci/typos/compare/v1.3.2...v1.3.3 [1.3.2]: https://redirect.github.com/crate-ci/typos/compare/v1.3.1...v1.3.2 [1.3.1]: https://redirect.github.com/crate-ci/typos/compare/v1.3.0...v1.3.1 [1.3.0]: https://redirect.github.com/crate-ci/typos/compare/v1.2.1...v1.3.0 [1.2.1]: https://redirect.github.com/crate-ci/typos/compare/v1.2.0...v1.2.1 [1.2.0]: https://redirect.github.com/crate-ci/typos/compare/v1.1.9...v1.2.0 [1.1.9]: https://redirect.github.com/crate-ci/typos/compare/v1.1.8...v1.1.9 [1.1.8]: https://redirect.github.com/crate-ci/typos/compare/v1.1.7...v1.1.8 [1.1.7]: https://redirect.github.com/crate-ci/typos/compare/v1.1.6...v1.1.7 [1.1.6]: https://redirect.github.com/crate-ci/typos/compare/v1.1.5...v1.1.6 [1.1.5]: https://redirect.github.com/crate-ci/typos/compare/v1.1.4...v1.1.5 [1.1.4]: https://redirect.github.com/crate-ci/typos/compare/v1.1.3...v1.1.4 [1.1.3]: https://redirect.github.com/crate-ci/typos/compare/v1.1.2...v1.1.3 [1.1.2]: https://redirect.github.com/crate-ci/typos/compare/v1.1.1...v1.1.2 [1.1.1]: https://redirect.github.com/crate-ci/typos/compare/v1.1.0...v1.1.1 [1.1.0]: https://redirect.github.com/crate-ci/typos/compare/v1.0.11...v1.1.0 [1.0.11]: https://redirect.github.com/crate-ci/typos/compare/v1.0.10...v1.0.11 [1.0.10]: https://redirect.github.com/crate-ci/typos/compare/v1.0.9...v1.0.10 [1.0.9]: https://redirect.github.com/crate-ci/typos/compare/v1.0.8...v1.0.9 [1.0.8]: https://redirect.github.com/crate-ci/typos/compare/v1.0.7...v1.0.8 [1.0.7]: https://redirect.github.com/crate-ci/typos/compare/v1.0.6...v1.0.7 [1.0.6]: https://redirect.github.com/crate-ci/typos/compare/v1.0.5...v1.0.6 [1.0.5]: https://redirect.github.com/crate-ci/typos/compare/v1.0.4...v1.0.5 [1.0.4]: https://redirect.github.com/crate-ci/typos/compare/v1.0.3...v1.0.4 [1.0.3]: https://redirect.github.com/crate-ci/typos/compare/v1.0.2...v1.0.3 [1.0.2]: https://redirect.github.com/crate-ci/typos/compare/v1.0.1...v1.0.2 [1.0.1]: https://redirect.github.com/crate-ci/typos/compare/v1.0.0...v1.0.1 [1.0.0]: https://redirect.github.com/crate-ci/typos/compare/v0.4.0...v1.0.0 [0.4.0]: https://redirect.github.com/crate-ci/typos/compare/v0.3.0...v0.4.0 [0.3.0]: https://redirect.github.com/crate-ci/typos/compare/v0.2.0...v0.3.0 [0.2.0]: https://redirect.github.com/crate-ci/typos/compare/v0.1.4...v0.2.0
python-jsonschema/check-jsonschema (python-jsonschema/check-jsonschema) ### [`v0.36.2`](https://redirect.github.com/python-jsonschema/check-jsonschema/blob/HEAD/CHANGELOG.rst#0362) [Compare Source](https://redirect.github.com/python-jsonschema/check-jsonschema/compare/0.36.1...0.36.2) - Update vendored schemas: circle-ci, gitlab-ci, mergify, renovate, snapcraft, woodpecker-ci (2026-02-15)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Waygood --- .pre-commit-config.yaml | 8 ++++---- python/ruff-ecosystem/pyproject.toml | 2 +- python/ruff-ecosystem/ruff_ecosystem/check.py | 11 +++++------ python/ruff-ecosystem/ruff_ecosystem/cli.py | 4 ++-- python/ruff-ecosystem/ruff_ecosystem/format.py | 13 +++++-------- python/ruff-ecosystem/ruff_ecosystem/main.py | 5 ++--- .../ruff-ecosystem/ruff_ecosystem/projects.py | 2 +- python/ruff-ecosystem/ruff_ecosystem/types.py | 17 ++++++++--------- 8 files changed, 28 insertions(+), 34 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fe74794b02b2..63152168f1b5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: priority: 0 - repo: https://github.com/crate-ci/typos - rev: v1.43.4 + rev: v1.43.5 hooks: - id: typos priority: 0 @@ -66,7 +66,7 @@ repos: priority: 0 - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.1 + rev: 0.36.2 hooks: - id: check-github-workflows priority: 0 @@ -93,7 +93,7 @@ repos: priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.1 + rev: v0.15.2 hooks: - id: ruff-format priority: 0 @@ -117,7 +117,7 @@ repos: # Priority 2: ruffen-docs runs after markdownlint-fix (both modify markdown). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.1 + rev: v0.15.2 hooks: - id: ruff-format name: mdtest format diff --git a/python/ruff-ecosystem/pyproject.toml b/python/ruff-ecosystem/pyproject.toml index 14a57a15489b0..69d5e2b7de2cc 100644 --- a/python/ruff-ecosystem/pyproject.toml +++ b/python/ruff-ecosystem/pyproject.toml @@ -12,5 +12,5 @@ dependencies = ["unidiff==0.7.5", "tomli_w==1.2.0", "tomli==2.4.0"] ruff-ecosystem = "ruff_ecosystem.cli:entrypoint" [tool.ruff.lint] -extend-select = ["I"] +ignore = ["T100"] preview = true diff --git a/python/ruff-ecosystem/ruff_ecosystem/check.py b/python/ruff-ecosystem/ruff_ecosystem/check.py index 6babd762d4641..b01faa3ff553e 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/check.py +++ b/python/ruff-ecosystem/ruff_ecosystem/check.py @@ -10,10 +10,11 @@ import time from asyncio import create_subprocess_exec from collections import Counter +from collections.abc import Iterable, Iterator, Sequence from dataclasses import dataclass, field from pathlib import Path from subprocess import PIPE -from typing import TYPE_CHECKING, Iterable, Iterator, Self, Sequence +from typing import TYPE_CHECKING, Self from ruff_ecosystem import logger from ruff_ecosystem.markdown import ( @@ -417,7 +418,7 @@ def try_from_string(cls: type[Self], line: str) -> Self | None: if match is None: # Handle case where there are no regex match e.g. - # + "?application=AIRFLOW&authenticator=TEST_AUTH&role=TEST_ROLE&warehouse=TEST_WAREHOUSE" # noqa: E501, ERA001 + # + "?application=AIRFLOW&authenticator=TEST_AUTH&role=TEST_ROLE&warehouse=TEST_WAREHOUSE" # Which was found in local testing return None @@ -457,7 +458,7 @@ def from_simple_diff(cls, diff: Diff) -> CheckDiff: diff = diff.without_unchanged_lines() # Sort without account for the leading + / - - sorted_lines = list(sorted(diff, key=lambda line: line[2:])) + sorted_lines = sorted(diff, key=lambda line: line[2:]) # Parse the lines, drop lines that cannot be parsed parsed_lines: list[DiagnosticLine] = list( @@ -559,10 +560,8 @@ async def ruff_check( raise ToolError(err.decode("utf8")) # Strip summary lines so the diff is only diagnostic lines - lines = [ + return [ line for line in result.decode("utf8").splitlines() if not CHECK_SUMMARY_LINE_RE.match(line) ] - - return lines diff --git a/python/ruff-ecosystem/ruff_ecosystem/cli.py b/python/ruff-ecosystem/ruff_ecosystem/cli.py index b996504c22b21..faf324d5f101d 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/cli.py +++ b/python/ruff-ecosystem/ruff_ecosystem/cli.py @@ -54,7 +54,7 @@ def entrypoint(): f"Could not find ruff baseline executable: {args.baseline_executable}", sys.stderr, ) - exit(1) + sys.exit(1) logger.info( "Resolved baseline executable %s to %s", args.baseline_executable, @@ -69,7 +69,7 @@ def entrypoint(): f"Could not find ruff comparison executable: {args.comparison_executable}", sys.stderr, ) - exit(1) + sys.exit(1) logger.info( "Resolved comparison executable %s to %s", args.comparison_executable, diff --git a/python/ruff-ecosystem/ruff_ecosystem/format.py b/python/ruff-ecosystem/ruff_ecosystem/format.py index fbb561d248139..b503a51dc6dc1 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/format.py +++ b/python/ruff-ecosystem/ruff_ecosystem/format.py @@ -6,10 +6,11 @@ import time from asyncio import create_subprocess_exec +from collections.abc import Sequence from enum import Enum from pathlib import Path from subprocess import PIPE -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING from unidiff import PatchSet @@ -182,7 +183,7 @@ async def format_then_format( options=options, ) # Then get the diff from stdout - diff = await format( + return await format( formatter=Formatter.ruff, executable=ruff_comparison_executable.resolve(), path=cloned_repo.path, @@ -190,7 +191,6 @@ async def format_then_format( options=options, diff=True, ) - return diff async def format_and_format( @@ -229,9 +229,7 @@ async def format_and_format( ) # Then get the diff from the commit - diff = await cloned_repo.diff(commit) - - return diff + return await cloned_repo.diff(commit) async def format( @@ -271,8 +269,7 @@ async def format( if proc.returncode not in [0, 1]: raise ToolError(err.decode("utf8")) - lines = result.decode("utf8").splitlines() - return lines + return result.decode("utf8").splitlines() class FormatComparison(Enum): diff --git a/python/ruff-ecosystem/ruff_ecosystem/main.py b/python/ruff-ecosystem/ruff_ecosystem/main.py index 4a3fc95e1482a..b2573fd3a9fc0 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/main.py +++ b/python/ruff-ecosystem/ruff_ecosystem/main.py @@ -1,9 +1,10 @@ import asyncio import dataclasses import json +from collections.abc import Awaitable from enum import Enum from pathlib import Path -from typing import Awaitable, TypeVar +from typing import TypeVar from ruff_ecosystem import logger from ruff_ecosystem.check import compare_check, markdown_check_result @@ -96,8 +97,6 @@ async def limited_parallelism(coroutine: Awaitable[T]) -> T: case _: raise ValueError(f"Unknown output format {format}") - return None - async def clone_and_compare( command: RuffCommand, diff --git a/python/ruff-ecosystem/ruff_ecosystem/projects.py b/python/ruff-ecosystem/ruff_ecosystem/projects.py index 38d5623159043..e25d2fd10fb94 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/projects.py +++ b/python/ruff-ecosystem/ruff_ecosystem/projects.py @@ -129,7 +129,7 @@ def patch_config( toml = {} # Do not write a toml file if it does not exist and we're just nulling values - if all((value is None for value in overrides.values())): + if all(value is None for value in overrides.values()): yield return diff --git a/python/ruff-ecosystem/ruff_ecosystem/types.py b/python/ruff-ecosystem/ruff_ecosystem/types.py index e9b9664aeea44..e3e2fcf3659e5 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/types.py +++ b/python/ruff-ecosystem/ruff_ecosystem/types.py @@ -3,8 +3,9 @@ import abc import dataclasses import difflib +from collections.abc import Iterable, Iterator, Sequence from dataclasses import dataclass, is_dataclass -from typing import TYPE_CHECKING, Any, Generator, Iterable, Sequence +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from ruff_ecosystem.projects import ClonedRepository, Project @@ -28,25 +29,25 @@ def __init__(self, lines: Iterable[str], leading_spaces: int = 0) -> None: self.lines = list(lines) # Compute added and removed lines once - self.added = list( + self.added = [ line[2:] for line in self.lines if line.startswith("+" + " " * leading_spaces) # Do not include patch headers and not line.startswith("+++") - ) - self.removed = list( + ] + self.removed = [ line[2:] for line in self.lines if line.startswith("-" + " " * leading_spaces) # Do not include patch headers and not line.startswith("---") - ) + ] def __bool__(self) -> bool: return bool(self.added or self.removed) - def __iter__(self) -> Generator[str, None, None]: + def __iter__(self) -> Iterator[str]: yield from self.lines @property @@ -65,9 +66,7 @@ def from_pair(cls, baseline: Sequence[str], comparison: Sequence[str]): return cls(difflib.ndiff(baseline, comparison), leading_spaces=1) def without_unchanged_lines(self) -> Diff: - return Diff( - line for line in self.lines if line.startswith("+") or line.startswith("-") - ) + return Diff(line for line in self.lines if line.startswith(("+", "-"))) def jsonable(self) -> Any: return self.lines From 313336b61bba043a636084e6047eccaea9a4e773 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:30:03 +0000 Subject: [PATCH 166/261] [ty] Add partial support and validation for `Unpack` when used with tuple types (#23651) Co-authored-by: Alex Waygood --- .../resources/mdtest/annotations/invalid.md | 9 +- .../resources/mdtest/annotations/starred.md | 6 + .../annotations/unsupported_special_forms.md | 6 + .../mdtest/assignment/annotations.md | 2 +- ...d_exp\342\200\246_(3fbab22ead236138).snap" | 105 ++++++++++++------ ...ithin\342\200\246_(3259718bf20b45a2).snap" | 1 - ...ithin\342\200\246_(711fb86287c4d87b).snap" | 1 - ...n_wit\342\200\246_(f58a51442a16371e).snap" | 1 - ...withi\342\200\246_(c19e9277cf9fafb5).snap" | 1 - .../src/types/infer/builder.rs | 8 ++ .../types/infer/builder/type_expression.rs | 36 +++++- 11 files changed, 133 insertions(+), 43 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 416fa3c38c646..d496ee0186475 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -165,20 +165,27 @@ python-version = "3.11" ``` ```py -from typing import TypeVarTuple +from typing import TypeVarTuple, Unpack Ts = TypeVarTuple("Ts") def f( # error: [invalid-type-form] "Multiple unpacked variadic tuples are not allowed in a `tuple` specialization" x: tuple[*tuple[int, ...], *tuple[str, ...]], + # error: [invalid-type-form] "Multiple unpacked variadic tuples are not allowed in a `tuple` specialization" + x2: tuple[Unpack[tuple[int, ...]], Unpack[tuple[str, ...]]], y: tuple[*tuple[int, ...], str, int, *tuple[str, ...]], # error: [invalid-type-form] + y2: tuple[Unpack[tuple[int, ...]], str, int, Unpack[tuple[str, ...]]], # error: [invalid-type-form] # Multiple unpacked elements are fine, as long as the unpacked elements are not variadic: z: tuple[*tuple[int, ...], *tuple[str]], + z2: tuple[Unpack[tuple[int, ...]], Unpack[tuple[str]]], ): reveal_type(x) # revealed: tuple[int | str, ...] + reveal_type(x2) # revealed: tuple[int | str, ...] reveal_type(y) # revealed: tuple[str | int, ...] + reveal_type(y2) # revealed: tuple[str | int, ...] reveal_type(z) # revealed: tuple[*tuple[int, ...], str] + reveal_type(z2) # revealed: tuple[*tuple[int, ...], str] T1 = tuple[int, *Ts, str, *Ts] # error: [invalid-type-form] diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md index d59b61a7c5c01..afcc1e89fa2b9 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md @@ -19,4 +19,10 @@ def append_int(*args: *Ts) -> tuple[*Ts, int]: # TODO should be tuple[Literal[True], Literal["a"], int] reveal_type(append_int(True, "a")) # revealed: tuple[@Todo(TypeVarTuple), ...] + +def first_arg_int(*args: *tuple[int, *tuple[str, ...]]): ... + +first_arg_int(42, "42", "42") # fine +first_arg_int("not an int", "42", "42") # TODO: should error +first_arg_int(56, "42", 56) # TODO: should error ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 50df54b556949..20a6240e4d5d2 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -41,6 +41,12 @@ def ex3(msg: str): return fn(*args, **kwargs) return wrapped return wrapper + +def first_arg_int(*args: Unpack[tuple[int, Unpack[tuple[str, ...]]]]): ... + +first_arg_int(42, "42", "42") # fine +first_arg_int("not an int", "42", "42") # TODO: should error +first_arg_int(56, "42", 56) # TODO: should error ``` ## Type expressions diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index c0af0e723d894..9772789ecc10e 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -69,7 +69,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]] reveal_type(e) # revealed: tuple[str, ...] reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes] -reveal_type(g) # revealed: tuple[@Todo(TypeVarTuple), ...] +reveal_type(g) # revealed: tuple[str, *tuple[int, ...], bytes] reveal_type(h) # revealed: tuple[list[int], list[int]] reveal_type(i) # revealed: tuple[str | int, str | int] diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" index c08051e4f67b6..3ed71cce9e689 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" @@ -13,26 +13,33 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md ## mdtest_snippet.py ``` - 1 | from typing import TypeVarTuple + 1 | from typing import TypeVarTuple, Unpack 2 | 3 | Ts = TypeVarTuple("Ts") 4 | 5 | def f( 6 | # error: [invalid-type-form] "Multiple unpacked variadic tuples are not allowed in a `tuple` specialization" 7 | x: tuple[*tuple[int, ...], *tuple[str, ...]], - 8 | y: tuple[*tuple[int, ...], str, int, *tuple[str, ...]], # error: [invalid-type-form] - 9 | # Multiple unpacked elements are fine, as long as the unpacked elements are not variadic: -10 | z: tuple[*tuple[int, ...], *tuple[str]], -11 | ): -12 | reveal_type(x) # revealed: tuple[int | str, ...] -13 | reveal_type(y) # revealed: tuple[str | int, ...] -14 | reveal_type(z) # revealed: tuple[*tuple[int, ...], str] -15 | -16 | T1 = tuple[int, *Ts, str, *Ts] # error: [invalid-type-form] -17 | -18 | def func3(t: tuple[*Ts]): -19 | t5: tuple[*tuple[str], *Ts] # OK -20 | t6: tuple[*tuple[str, ...], *Ts] # error: [invalid-type-form] + 8 | # error: [invalid-type-form] "Multiple unpacked variadic tuples are not allowed in a `tuple` specialization" + 9 | x2: tuple[Unpack[tuple[int, ...]], Unpack[tuple[str, ...]]], +10 | y: tuple[*tuple[int, ...], str, int, *tuple[str, ...]], # error: [invalid-type-form] +11 | y2: tuple[Unpack[tuple[int, ...]], str, int, Unpack[tuple[str, ...]]], # error: [invalid-type-form] +12 | # Multiple unpacked elements are fine, as long as the unpacked elements are not variadic: +13 | z: tuple[*tuple[int, ...], *tuple[str]], +14 | z2: tuple[Unpack[tuple[int, ...]], Unpack[tuple[str]]], +15 | ): +16 | reveal_type(x) # revealed: tuple[int | str, ...] +17 | reveal_type(x2) # revealed: tuple[int | str, ...] +18 | reveal_type(y) # revealed: tuple[str | int, ...] +19 | reveal_type(y2) # revealed: tuple[str | int, ...] +20 | reveal_type(z) # revealed: tuple[*tuple[int, ...], str] +21 | reveal_type(z2) # revealed: tuple[*tuple[int, ...], str] +22 | +23 | T1 = tuple[int, *Ts, str, *Ts] # error: [invalid-type-form] +24 | +25 | def func3(t: tuple[*Ts]): +26 | t5: tuple[*tuple[str], *Ts] # OK +27 | t6: tuple[*tuple[str, ...], *Ts] # error: [invalid-type-form] ``` # Diagnostics @@ -48,8 +55,8 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple -8 | y: tuple[*tuple[int, ...], str, int, *tuple[str, ...]], # error: [invalid-type-form] -9 | # Multiple unpacked elements are fine, as long as the unpacked elements are not variadic: +8 | # error: [invalid-type-form] "Multiple unpacked variadic tuples are not allowed in a `tuple` specialization" +9 | x2: tuple[Unpack[tuple[int, ...]], Unpack[tuple[str, ...]]], | info: rule `invalid-type-form` is enabled by default @@ -57,17 +64,53 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a `tuple` specialization - --> src/mdtest_snippet.py:8:8 + --> src/mdtest_snippet.py:9:9 | - 6 | # error: [invalid-type-form] "Multiple unpacked variadic tuples are not allowed in a `tuple` specialization" 7 | x: tuple[*tuple[int, ...], *tuple[str, ...]], - 8 | y: tuple[*tuple[int, ...], str, int, *tuple[str, ...]], # error: [invalid-type-form] + 8 | # error: [invalid-type-form] "Multiple unpacked variadic tuples are not allowed in a `tuple` specialization" + 9 | x2: tuple[Unpack[tuple[int, ...]], Unpack[tuple[str, ...]]], + | ^^^^^^-----------------------^^-----------------------^ + | | | + | | Later unpacked variadic tuple + | First unpacked variadic tuple +10 | y: tuple[*tuple[int, ...], str, int, *tuple[str, ...]], # error: [invalid-type-form] +11 | y2: tuple[Unpack[tuple[int, ...]], str, int, Unpack[tuple[str, ...]]], # error: [invalid-type-form] + | +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a `tuple` specialization + --> src/mdtest_snippet.py:10:8 + | + 8 | # error: [invalid-type-form] "Multiple unpacked variadic tuples are not allowed in a `tuple` specialization" + 9 | x2: tuple[Unpack[tuple[int, ...]], Unpack[tuple[str, ...]]], +10 | y: tuple[*tuple[int, ...], str, int, *tuple[str, ...]], # error: [invalid-type-form] | ^^^^^^----------------^^^^^^^^^^^^----------------^ | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - 9 | # Multiple unpacked elements are fine, as long as the unpacked elements are not variadic: -10 | z: tuple[*tuple[int, ...], *tuple[str]], +11 | y2: tuple[Unpack[tuple[int, ...]], str, int, Unpack[tuple[str, ...]]], # error: [invalid-type-form] +12 | # Multiple unpacked elements are fine, as long as the unpacked elements are not variadic: + | +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a `tuple` specialization + --> src/mdtest_snippet.py:11:9 + | + 9 | x2: tuple[Unpack[tuple[int, ...]], Unpack[tuple[str, ...]]], +10 | y: tuple[*tuple[int, ...], str, int, *tuple[str, ...]], # error: [invalid-type-form] +11 | y2: tuple[Unpack[tuple[int, ...]], str, int, Unpack[tuple[str, ...]]], # error: [invalid-type-form] + | ^^^^^^-----------------------^^^^^^^^^^^^-----------------------^ + | | | + | | Later unpacked variadic tuple + | First unpacked variadic tuple +12 | # Multiple unpacked elements are fine, as long as the unpacked elements are not variadic: +13 | z: tuple[*tuple[int, ...], *tuple[str]], | info: rule `invalid-type-form` is enabled by default @@ -75,17 +118,17 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a `tuple` specialization - --> src/mdtest_snippet.py:16:6 + --> src/mdtest_snippet.py:23:6 | -14 | reveal_type(z) # revealed: tuple[*tuple[int, ...], str] -15 | -16 | T1 = tuple[int, *Ts, str, *Ts] # error: [invalid-type-form] +21 | reveal_type(z2) # revealed: tuple[*tuple[int, ...], str] +22 | +23 | T1 = tuple[int, *Ts, str, *Ts] # error: [invalid-type-form] | ^^^^^^^^^^^---^^^^^^^---^ | | | | | Later unpacked variadic tuple | First unpacked variadic tuple -17 | -18 | def func3(t: tuple[*Ts]): +24 | +25 | def func3(t: tuple[*Ts]): | info: rule `invalid-type-form` is enabled by default @@ -93,11 +136,11 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a `tuple` specialization - --> src/mdtest_snippet.py:20:9 + --> src/mdtest_snippet.py:27:9 | -18 | def func3(t: tuple[*Ts]): -19 | t5: tuple[*tuple[str], *Ts] # OK -20 | t6: tuple[*tuple[str, ...], *Ts] # error: [invalid-type-form] +25 | def func3(t: tuple[*Ts]): +26 | t5: tuple[*tuple[str], *Ts] # OK +27 | t6: tuple[*tuple[str, ...], *Ts] # error: [invalid-type-form] | ^^^^^^----------------^^---^ | | | | | Later unpacked variadic tuple diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" index 478268091b0ae..ac8c462198c71 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" @@ -1,6 +1,5 @@ --- source: crates/ty_test/src/lib.rs -assertion_line: 624 expression: snapshot --- diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" index bf823b7861094..33f13986a9a60 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" @@ -1,6 +1,5 @@ --- source: crates/ty_test/src/lib.rs -assertion_line: 624 expression: snapshot --- diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" index 3646c6bff4e55..75e33084a6246 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" @@ -1,6 +1,5 @@ --- source: crates/ty_test/src/lib.rs -assertion_line: 624 expression: snapshot --- diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" index 39b648f1c9269..6fe4af331fc91 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" @@ -1,6 +1,5 @@ --- source: crates/ty_test/src/lib.rs -assertion_line: 624 expression: snapshot --- diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index cc60203aaf2fd..6754de4c9538c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -315,6 +315,8 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { /// While this is `Get`, any expressions will be considered to have already been inferred. inner_expression_inference_state: InnerExpressionInferenceState, + inferring_vararg_annotation: bool, + /// For function definitions, the undecorated type of the function. undecorated_type: Option>, @@ -353,6 +355,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return_types_and_ranges: vec![], called_functions: FxIndexSet::default(), deferred_state: DeferredExpressionState::None, + inferring_vararg_annotation: false, multi_inference_state: MultiInferenceState::Panic, inner_expression_inference_state: InnerExpressionInferenceState::Infer, expressions: FxHashMap::default(), @@ -3525,7 +3528,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_parameter_with_default(param_with_default); } if let Some(vararg) = vararg { + self.inferring_vararg_annotation = true; self.infer_parameter(vararg); + self.inferring_vararg_annotation = false; } if let Some(kwarg) = kwarg { self.infer_parameter(kwarg); @@ -14601,6 +14606,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { deferred_state: _, multi_inference_state: _, inner_expression_inference_state: _, + inferring_vararg_annotation: _, called_functions: _, index: _, region: _, @@ -14667,6 +14673,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { all_definitely_bound: _, typevar_binding_context: _, deferred_state: _, + inferring_vararg_annotation: _, multi_inference_state: _, inner_expression_inference_state: _, index: _, @@ -14750,6 +14757,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { deferred_state: _, multi_inference_state: _, inner_expression_inference_state: _, + inferring_vararg_annotation: _, called_functions: _, index: _, region: _, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index cdffa66228759..e593b3ea275e0 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -660,11 +660,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return_todo |= element_could_alter_type_of_whole_tuple(element, element_ty, self); - if let ast::Expr::Starred(ast::ExprStarred { - value: starred_value, - .. + // Determine if this element unpacks a tuple: either `*expr` or `Unpack[expr]` + let unpack_inner = if let ast::Expr::Starred(ast::ExprStarred { + value, .. }) = element { + Some(&**value) + } else if let ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = + element + && self.expression_type(value) == Type::SpecialForm(SpecialFormType::Unpack) + { + Some(&**slice) + } else { + None + }; + + if let Some(unpack_inner) = unpack_inner { let mut report_too_many_unpacked_tuples = || { if let Some(first_unpacked_variadic_tuple) = first_unpacked_variadic_tuple @@ -698,7 +709,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if inner_tuple.is_variadic() { report_too_many_unpacked_tuples(); } - } else if self.expression_type(starred_value) + } else if self.expression_type(unpack_inner) == Type::Dynamic(DynamicType::TodoTypeVarTuple) { report_too_many_unpacked_tuples(); @@ -1672,8 +1683,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { inferred_type } SpecialFormType::Unpack => { - self.infer_type_expression(arguments_slice); - todo_type!("`Unpack[]` special form") + let inner_ty = self.infer_type_expression(arguments_slice); + + // When the argument is a tuple type, return it directly so that + // `Unpack[tuple[int, ...]]` behaves identically to `*tuple[int, ...]`. + // + // However, we still need a Todo type for things like + // `def f(*args: Unpack[tuple[int, Unpack[tuple[str, ...]]]]): ...`, + // which we don't yet support. + if self.inferring_vararg_annotation + || inner_ty.exact_tuple_instance_spec(self.db()).is_none() + { + todo_type!("`Unpack[]` special form") + } else { + inner_ty + } } SpecialFormType::NoReturn | SpecialFormType::Never From aa341f5e45bb205a24216f8d7fac0a816af50551 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 2 Mar 2026 10:29:24 -0500 Subject: [PATCH 167/261] [ty] Remove `specialize_constrained` from constraint set module (#23677) I had added `specialize_constrained` as a way to create a `Specialization` of a `GenericContext` from a constraint set. But the new solver in `SpecializationBuilder` does not use it; is generates a list of `Solutions` separately, so that it can combine the constraint set solutions with existing solutions from the old solver. That makes `specialize_constrained` redundant. This PR removes it, to lessen confusion about how we're actually creating specializations from a constraint set. --- .../mdtest/generics/specialize_constrained.md | 444 ------------------ crates/ty_python_semantic/src/types.rs | 51 +- .../ty_python_semantic/src/types/call/bind.rs | 22 - .../src/types/constraints.rs | 295 +----------- .../ty_python_semantic/src/types/display.rs | 3 - .../ty_extensions/ty_extensions.pyi | 8 - 6 files changed, 7 insertions(+), 816 deletions(-) delete mode 100644 crates/ty_python_semantic/resources/mdtest/generics/specialize_constrained.md diff --git a/crates/ty_python_semantic/resources/mdtest/generics/specialize_constrained.md b/crates/ty_python_semantic/resources/mdtest/generics/specialize_constrained.md deleted file mode 100644 index 32956cdfa8f6a..0000000000000 --- a/crates/ty_python_semantic/resources/mdtest/generics/specialize_constrained.md +++ /dev/null @@ -1,444 +0,0 @@ -# Creating a specialization from a constraint set - -```toml -[environment] -python-version = "3.12" -``` - -We create constraint sets to describe which types a set of typevars can specialize to. We have a -`specialize_constrained` method that creates a "best" specialization for a constraint set, which -lets us test this logic in isolation, without having to bring in the rest of the specialization -inference logic. - -## Unbounded typevars - -An unbounded typevar can specialize to any type. We will specialize the typevar to the least upper -bound of all of the types that satisfy the constraint set. - -```py -from typing import Any, Never -from ty_extensions import ConstraintSet, generic_context - -# fmt: off - -def unbounded[T](): - # revealed: ty_extensions.Specialization[T@unbounded = Unknown] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.always())) - # revealed: ty_extensions.Specialization[T@unbounded = object] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, object))) - # revealed: ty_extensions.Specialization[T@unbounded = Any] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, Any))) - # revealed: None - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@unbounded = int] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int))) - # revealed: ty_extensions.Specialization[T@unbounded = int] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(bool, T, int))) - - # revealed: ty_extensions.Specialization[T@unbounded = bool] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, T, bool))) - # revealed: ty_extensions.Specialization[T@unbounded = Never] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, T, str))) - # revealed: None - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(bool, T, bool) & ConstraintSet.range(Never, T, str))) - - # TODO: revealed: ty_extensions.Specialization[T@unbounded = int] - # revealed: ty_extensions.Specialization[T@unbounded = bool] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, bool))) - # revealed: ty_extensions.Specialization[T@unbounded = Never] - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, str))) - # revealed: None - reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(bool, T, bool) | ConstraintSet.range(Never, T, str))) -``` - -## Typevar with an upper bound - -If a typevar has an upper bound, then it must specialize to a type that is a subtype of that bound. - -```py -from typing import final, Never -from ty_extensions import ConstraintSet, generic_context - -class Super: ... -class Base(Super): ... -class Sub(Base): ... - -@final -class Unrelated: ... - -def bounded[T: Base](): - # revealed: ty_extensions.Specialization[T@bounded = Base] - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.always())) - # revealed: ty_extensions.Specialization[T@bounded = Base] - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, object))) - # revealed: ty_extensions.Specialization[T@bounded = Base & Any] - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Any))) - # revealed: None - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@bounded = Base] - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Super))) - # revealed: ty_extensions.Specialization[T@bounded = Base] - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Base))) - # revealed: ty_extensions.Specialization[T@bounded = Sub] - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Sub))) - - # revealed: ty_extensions.Specialization[T@bounded = Never] - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Unrelated))) - # revealed: None - reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Unrelated, T, Unrelated))) -``` - -If the upper bound is a gradual type, we are free to choose any materialization of the upper bound -that makes the test succeed. - -```py -from typing import Any - -def bounded_by_gradual[T: Any](): - # TODO: revealed: ty_extensions.Specialization[T@bounded_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@bounded_by_gradual = object] - reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.always())) - # revealed: ty_extensions.Specialization[T@bounded_by_gradual = object] - reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, object))) - # revealed: ty_extensions.Specialization[T@bounded_by_gradual = Any] - reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Any))) - # revealed: None - reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@bounded_by_gradual = Base] - reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base))) - # revealed: ty_extensions.Specialization[T@bounded_by_gradual = object] - reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.range(Base, T, object))) - - # revealed: ty_extensions.Specialization[T@bounded_by_gradual = Unrelated] - reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated))) - -def bounded_by_gradual_list[T: list[Any]](): - # revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = Top[list[Any]]] - reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.always())) - # revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = list[object]] - reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[object]))) - # revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = list[Any]] - reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Any]))) - # revealed: None - reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = list[Base]] - reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Base]))) - # TODO: revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = list[Base]] - # revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = Top[list[Any]]] - reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.range(list[Base], T, object))) - - # revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = list[Unrelated]] - reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Unrelated]))) - # TODO: revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = list[Unrelated]] - # revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = Top[list[Any]]] - reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.range(list[Unrelated], T, object))) -``` - -## Constrained typevar - -If a typevar has constraints, then it must specialize to one of those specific types. (Not to a -subtype of one of those types!) - -In particular, note that if a constraint set is satisfied by more than one of the typevar's -constraints (i.e., we have no reason to prefer one over the others), then we return `None` to -indicate an ambiguous result. We could, in theory, return _more than one_ specialization, since we -have all of the information necessary to produce this. But it's not clear what we would do with that -information at the moment. - -```py -from typing import final, Never -from ty_extensions import ConstraintSet, generic_context - -class Super: ... -class Base(Super): ... -class Sub(Base): ... - -@final -class Unrelated: ... - -def constrained[T: (Base, Unrelated)](): - # revealed: None - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.always())) - # revealed: None - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Never, T, object))) - # revealed: None - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Never, T, Any))) - # revealed: None - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@constrained = Base] - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Never, T, Base))) - # revealed: ty_extensions.Specialization[T@constrained = Base] - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Base, T, object))) - - # revealed: ty_extensions.Specialization[T@constrained = Unrelated] - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Never, T, Unrelated))) - # revealed: ty_extensions.Specialization[T@constrained = Unrelated] - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Unrelated, T, object))) - - # revealed: ty_extensions.Specialization[T@constrained = Base] - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Never, T, Super))) - # revealed: None - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Super, T, Super))) - - # revealed: ty_extensions.Specialization[T@constrained = Base] - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Sub, T, object))) - # revealed: None - reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Sub, T, Sub))) -``` - -If any of the constraints is a gradual type, we are free to choose any materialization of that -constraint that makes the test succeed. - -TODO: At the moment, we are producing a specialization that shows which particular materialization -that we chose, but really, we should be returning the gradual constraint as the specialization. - -```py -from typing import Any - -# fmt: off - -def constrained_by_gradual[T: (Base, Any)](): - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Unknown] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.always())) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, object))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base & Any] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Any))) - # revealed: None - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.never())) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Base, T, object))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Unrelated] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = object] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Unrelated, T, object))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Super))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Super, T, Super))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Sub, T, object))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual = Sub] - reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Sub, T, Sub))) - -def constrained_by_two_gradual[T: (Any, Any)](): - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = object] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.always())) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = object] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Never, T, object))) - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Any] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Never, T, Any))) - # revealed: None - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.never())) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Base] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Unrelated] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Super] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Never, T, Super))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Super] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Super, T, Super))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = object] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Sub, T, object))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Sub] - reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Sub, T, Sub))) - -def constrained_by_gradual_list[T: (list[Base], list[Any])](): - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Base]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.always())) - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[object]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[object]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Base] & list[Any]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Any]))) - # revealed: None - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Base]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Base]))) - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Base]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(list[Base], T, object))) - - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Unrelated]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Unrelated]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Unrelated]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = Top[list[Any]]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(list[Unrelated], T, object))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Super]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Super]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Super]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(list[Super], T, list[Super]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Sub]] - reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(list[Sub], T, list[Sub]))) - -# Same tests as above, but with the typevar constraints in a different order, to make sure the -# results do not depend on our BDD variable ordering. -def constrained_by_gradual_list_reverse[T: (list[Any], list[Base])](): - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Base]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.always())) - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[object]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(Never, T, list[object]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Base] & list[Any]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(Never, T, list[Any]))) - # revealed: None - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Base]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(Never, T, list[Base]))) - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Base]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(list[Base], T, object))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Unrelated]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(Never, T, list[Unrelated]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Unrelated]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = Top[list[Any]]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(list[Unrelated], T, object))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Super]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(Never, T, list[Super]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Super]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(list[Super], T, list[Super]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_gradual_list_reverse = list[Sub]] - reveal_type(generic_context(constrained_by_gradual_list_reverse).specialize_constrained(ConstraintSet.range(list[Sub], T, list[Sub]))) - -def constrained_by_two_gradual_lists[T: (list[Any], list[Any])](): - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = Top[list[Any]]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.always())) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = Top[list[Any]]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(Never, T, object))) - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Any]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(Never, T, list[Any]))) - # revealed: None - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Base]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(Never, T, list[Base]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Base]] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = Top[list[Any]]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(list[Base], T, object))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Unrelated]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(Never, T, list[Unrelated]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Unrelated]] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = Top[list[Any]]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(list[Unrelated], T, object))) - - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Super]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(Never, T, list[Super]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Super]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(list[Super], T, list[Super]))) - # TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]] - # revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Sub]] - reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(list[Sub], T, list[Sub]))) -``` - -## Mutually constrained typevars - -If one typevar is constrained by another, the specialization of one can affect the specialization of -the other. - -```py -from typing import final, Never -from ty_extensions import ConstraintSet, generic_context - -class Super: ... -class Base(Super): ... -class Sub(Base): ... - -@final -class Unrelated: ... - -# fmt: off - -def mutually_bound[T: Base, U](): - # revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Unknown] - reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.always())) - # revealed: None - reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.never())) - - # revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Base] - reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, U, T))) - - # revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Unknown] - reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub))) - # revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Sub] - reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub) & ConstraintSet.range(Never, U, T))) - # revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Sub] - reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, U, Sub) & ConstraintSet.range(Never, U, T))) -``` - -## Nested typevars - -A typevar's constraint can _mention_ another typevar without _constraining_ it. In this example, `U` -must be specialized to `list[T]`, but it cannot affect what `T` is specialized to. - -```py -from typing import Never -from ty_extensions import ConstraintSet, generic_context - -def mentions[T, U](): - # (T@mentions ≤ int) ∧ (U@mentions = list[T@mentions]) - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(list[T], U, list[T]) - # TODO: revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]] - # revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = Unknown] - reveal_type(generic_context(mentions).specialize_constrained(constraints)) -``` - -If the constraint set contains mutually recursive bounds, specialization inference will not -converge. This test ensures that our cycle detection prevents an endless loop or stack overflow in -this case. - -```py -def divergent[T, U](): - # (T@divergent = list[U@divergent]) ∧ (U@divergent = list[T@divergent])) - constraints = ConstraintSet.range(list[U], T, list[U]) & ConstraintSet.range(list[T], U, list[T]) - # revealed: None - reveal_type(generic_context(divergent).specialize_constrained(constraints)) -``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 76235378fb2c3..a235186834af9 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -3212,14 +3212,6 @@ impl<'db> Type<'db> { )) .into() } - Type::KnownInstance(KnownInstanceType::GenericContext(tracked)) - if name == "specialize_constrained" => - { - Place::bound(Type::KnownBoundMethod( - KnownBoundMethodType::GenericContextSpecializeConstrained(tracked), - )) - .into() - } Type::ClassLiteral(class) if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => @@ -6438,7 +6430,6 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_) ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) @@ -6668,8 +6659,7 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_), + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) @@ -10748,9 +10738,6 @@ pub enum KnownBoundMethodType<'db> { ConstraintSetImpliesSubtypeOf(InternedConstraintSet<'db>), ConstraintSetSatisfies(InternedConstraintSet<'db>), ConstraintSetSatisfiedByAllTypeVars(InternedConstraintSet<'db>), - - // GenericContext methods - GenericContextSpecializeConstrained(GenericContext<'db>), } pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -10782,8 +10769,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_) => {} + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {} } } @@ -10868,10 +10854,6 @@ impl<'db> KnownBoundMethodType<'db> { | ( KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - ) - | ( - KnownBoundMethodType::GenericContextSpecializeConstrained(_), - KnownBoundMethodType::GenericContextSpecializeConstrained(_), ) => ConstraintSet::from_bool(constraints, true), ( @@ -10885,8 +10867,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_), + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), KnownBoundMethodType::FunctionTypeDunderGet(_) | KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::PropertyDunderGet(_) @@ -10897,8 +10878,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_), + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), ) => ConstraintSet::from_bool(constraints, false), } } @@ -10936,8 +10916,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_) => Some(self), + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => Some(self), } } @@ -10954,8 +10933,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) - | KnownBoundMethodType::GenericContextSpecializeConstrained(_) => { + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { KnownClass::ConstraintSet } } @@ -11140,23 +11118,6 @@ impl<'db> KnownBoundMethodType<'db> { KnownClass::Bool.to_instance(db), ))) } - - KnownBoundMethodType::GenericContextSpecializeConstrained(_) => { - Either::Right(std::iter::once(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("constraints"))) - .with_annotated_type(KnownClass::ConstraintSet.to_instance(db)), - ], - ), - UnionType::from_two_elements( - db, - KnownClass::Specialization.to_instance(db), - Type::none(db), - ), - ))) - } } } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index ae7d999f4d8a8..f76ab6ca01672 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1877,28 +1877,6 @@ impl<'db> Bindings<'db> { overload.set_return_type(Type::bool_literal(result)); } - Type::KnownBoundMethod( - KnownBoundMethodType::GenericContextSpecializeConstrained(generic_context), - ) => { - let [Some(set)] = overload.parameter_types() else { - continue; - }; - let Type::KnownInstance(KnownInstanceType::ConstraintSet(set)) = set else { - continue; - }; - let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(set.constraints(db)); - let specialization = - generic_context.specialize_constrained(db, &constraints, set); - let result = match specialization { - Ok(specialization) => Type::KnownInstance( - KnownInstanceType::Specialization(specialization), - ), - Err(()) => Type::none(db), - }; - overload.set_return_type(result); - } - Type::ClassLiteral(class) => match class.known(db) { Some(KnownClass::Bool) => match overload.parameter_types() { [Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)), diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 95e1d4067585a..c2c15a6ad1943 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -79,7 +79,7 @@ use salsa::plumbing::AsId; use smallvec::SmallVec; use crate::types::class::GenericAlias; -use crate::types::generics::{GenericContext, InferableTypeVars, Specialization}; +use crate::types::generics::InferableTypeVars; use crate::types::visitor::{ TypeCollector, TypeVisitor, any_over_type, walk_type_with_recursion_guard, }; @@ -1638,16 +1638,6 @@ impl<'db> Node<'db> { } } - /// Returns a new BDD that is the _existential abstraction_ of `self` for a set of typevars. - /// All typevars _other_ than the one given will be removed and abstracted away. - fn retain_one(self, db: &'db dyn Db, bound_typevar: BoundTypeVarIdentity<'db>) -> Self { - match self { - Node::AlwaysTrue => Node::AlwaysTrue, - Node::AlwaysFalse => Node::AlwaysFalse, - Node::Interior(interior) => interior.retain_one(db, bound_typevar), - } - } - fn abstract_one_inner( self, db: &'db dyn Db, @@ -1661,103 +1651,6 @@ impl<'db> Node<'db> { } } - /// Invokes a callback for each of the representative types of a particular typevar for this - /// constraint set. - /// - /// We first abstract the BDD so that it only mentions constraints on the requested typevar. We - /// then invoke your callback for each distinct path from the BDD root to the `AlwaysTrue` - /// terminal. Each of those paths can be viewed as the conjunction of the individual - /// constraints of each internal node that we traverse as we walk that path. We provide the - /// lower/upper bound of this conjunction to your callback, allowing you to choose any suitable - /// type in the range. - /// - /// If the abstracted BDD does not mention the typevar at all (i.e., it leaves the typevar - /// completely unconstrained), we will invoke your callback once with `None`. - fn find_representative_types( - self, - db: &'db dyn Db, - bound_typevar: BoundTypeVarIdentity<'db>, - mut f: impl FnMut(Option<&[RepresentativeBounds<'db>]>), - ) { - self.retain_one(db, bound_typevar) - .find_representative_types_inner(db, &mut Vec::default(), &mut f); - } - - fn find_representative_types_inner( - self, - db: &'db dyn Db, - current_bounds: &mut Vec>, - f: &mut dyn FnMut(Option<&[RepresentativeBounds<'db>]>), - ) { - match self { - Node::AlwaysTrue => { - // If we reach the `true` terminal, the path we've been following represents one - // representative type. - if current_bounds.is_empty() { - f(None); - return; - } - - // If `lower ≰ upper`, then this path somehow represents in invalid specialization. - // That should have been removed from the BDD domain as part of the simplification - // process. (Here we are just checking assignability, so we don't need to construct - // the lower and upper bounds in a consistent order.) - debug_assert!({ - let greatest_lower_bound = UnionType::from_elements( - db, - current_bounds.iter().map(|bounds| bounds.lower), - ); - let least_upper_bound = IntersectionType::from_elements( - db, - current_bounds.iter().map(|bounds| bounds.upper), - ); - greatest_lower_bound.is_constraint_set_assignable_to(db, least_upper_bound) - }); - - // We've been tracking the lower and upper bound that the types for this path must - // satisfy. Pass those bounds along and let the caller choose a representative type - // from within that range. - f(Some(current_bounds)); - } - - Node::AlwaysFalse => { - // If we reach the `false` terminal, the path we've been following represents an - // invalid specialization, so we skip it. - } - - Node::Interior(interior) => { - let reset_point = current_bounds.len(); - - // For an interior node, there are two outgoing paths: one for the `if_true` - // branch, and one for the `if_false` branch. - // - // For the `if_true` branch, this node's constraint places additional restrictions - // on the types that satisfy the current path through the BDD. So we intersect the - // current glb/lub with the constraint's bounds to get the new glb/lub for the - // recursive call. - current_bounds.push(RepresentativeBounds::from_interior_node(db, interior)); - interior - .if_true(db) - .find_representative_types_inner(db, current_bounds, f); - current_bounds.truncate(reset_point); - - // For the `if_false` branch, then the types that satisfy the current path through - // the BDD do _not_ satisfy the node's constraint. Because we used `retain_one` to - // abstract the BDD to a single typevar, we don't need to worry about how that - // negative constraint affects the lower/upper bound that we're tracking. The - // abstraction process will have compared the negative constraint with all of the - // other constraints in the BDD, and added new interior nodes to handle the - // combination of those constraints. So we can recurse down the `if_false` branch - // without updating the lower/upper bounds, relying on the other constraints along - // the path to incorporate that negative "hole" in the set of valid types for this - // path. - interior - .if_false(db) - .find_representative_types_inner(db, current_bounds, f); - } - } - } - /// Returns a new BDD that returns the same results as `self`, but with some inputs fixed to /// particular values. (Those variables will not be checked when evaluating the result, and /// will not be present in the result.) @@ -2101,27 +1994,6 @@ impl<'db> Node<'db> { } } -#[derive(Clone, Copy, Debug)] -struct RepresentativeBounds<'db> { - lower: Type<'db>, - upper: Type<'db>, - source_order: usize, -} - -impl<'db> RepresentativeBounds<'db> { - fn from_interior_node(db: &'db dyn Db, interior: InteriorNode<'db>) -> Self { - let constraint = interior.constraint(db); - let lower = constraint.lower(db); - let upper = constraint.upper(db); - let source_order = interior.source_order(db); - Self { - lower, - upper, - source_order, - } - } -} - /// An interior node of a BDD #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] struct InteriorNode<'db> { @@ -2287,28 +2159,6 @@ impl<'db> InteriorNode<'db> { ) } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - fn retain_one(self, db: &'db dyn Db, bound_typevar: BoundTypeVarIdentity<'db>) -> Node<'db> { - let mut path = self.path_assignments(db); - self.abstract_one_inner( - db, - // Remove any node that constrains some other typevar than `bound_typevar`, and any - // node that constrains `bound_typevar` with a lower/upper bound of some other typevar. - // (For the latter, if there are any derived facts that we can infer from the typevar - // bound, those will be automatically added to the result.) - &mut |constraint| { - if constraint.typevar(db).identity(db) != bound_typevar { - return true; - } - if constraint.lower(db).has_typevar(db) || constraint.upper(db).has_typevar(db) { - return true; - } - false - }, - &mut path, - ) - } - fn abstract_one_inner( self, db: &'db dyn Db, @@ -4299,149 +4149,6 @@ impl<'db> BoundTypeVarInstance<'db> { } } -impl<'db> GenericContext<'db> { - pub(crate) fn specialize_constrained<'c>( - self, - db: &'db dyn Db, - _builder: &'c ConstraintSetBuilder<'db>, - constraints: ConstraintSet<'db, 'c>, - ) -> Result, ()> { - tracing::trace!( - target: "ty_python_semantic::types::constraints::specialize_constrained", - generic_context = %self.display_full(db), - constraints = %constraints.node.display(db), - "create specialization for constraint set", - ); - - // If the constraint set is cyclic, don't even try to construct a specialization. - if constraints.is_cyclic(db) { - tracing::error!( - target: "ty_python_semantic::types::constraints::specialize_constrained", - constraints = %constraints.node.display(db), - "constraint set is cyclic", - ); - // TODO: Better error - return Err(()); - } - - // First we intersect with the valid specializations of all of the typevars. We need all of - // valid specializations to hold simultaneously, so we do this once before abstracting over - // each typevar. - let abstracted = self - .variables(db) - .fold(Node::AlwaysTrue, |constraints, bound_typevar| { - constraints.and_with_offset(db, bound_typevar.valid_specializations(db)) - }) - .and_with_offset(db, constraints.node); - tracing::trace!( - target: "ty_python_semantic::types::constraints::specialize_constrained", - valid = %abstracted.display(db), - "limited to valid specializations", - ); - - // Then we find all of the "representative types" for each typevar in the constraint set. - let mut error_occurred = false; - let mut representatives = Vec::new(); - let types = - self.variables(db).map(|bound_typevar| { - // Each representative type represents one of the ways that the typevar can satisfy the - // constraint, expressed as a lower/upper bound on the types that the typevar can - // specialize to. - // - // If there are multiple paths in the BDD, they technically represent independent - // possible specializations. If there's a type that satisfies all of them, we will - // return that as the specialization. If not, then the constraint set is ambiguous. - // (This happens most often with constrained typevars.) We could in the future turn - // _each_ of the paths into separate specializations, but it's not clear what we would - // do with that, so instead we just report the ambiguity as a specialization failure. - let mut unconstrained = false; - let identity = bound_typevar.identity(db); - tracing::trace!( - target: "ty_python_semantic::types::constraints::specialize_constrained", - bound_typevar = %identity.display(db), - abstracted = %abstracted.retain_one(db, identity).display(db), - "find specialization for typevar", - ); - representatives.clear(); - abstracted.find_representative_types(db, identity, |representative| { - match representative { - Some(representative) => { - representatives.extend_from_slice(representative); - } - None => { - unconstrained = true; - } - } - }); - - // The BDD is satisfiable, but the typevar is unconstrained, then we use `None` to tell - // specialize_recursive to fall back on the typevar's default. - if unconstrained { - tracing::trace!( - target: "ty_python_semantic::types::constraints::specialize_constrained", - bound_typevar = %identity.display(db), - "typevar is unconstrained", - ); - return None; - } - - // If there are no satisfiable paths in the BDD, then there is no valid specialization - // for this constraint set. - if representatives.is_empty() { - // TODO: Construct a useful error here - tracing::trace!( - target: "ty_python_semantic::types::constraints::specialize_constrained", - bound_typevar = %identity.display(db), - "typevar cannot be satisfied", - ); - error_occurred = true; - return None; - } - - // Before constructing the final lower and upper bound, sort the constraints by - // their source order. This should give us a consistently ordered specialization, - // regardless of the variable ordering of the original BDD. - representatives.sort_unstable_by_key(|bounds| bounds.source_order); - let greatest_lower_bound = - UnionType::from_elements(db, representatives.iter().map(|bounds| bounds.lower)); - let least_upper_bound = IntersectionType::from_elements( - db, - representatives.iter().map(|bounds| bounds.upper), - ); - - // If `lower ≰ upper`, then there is no type that satisfies all of the paths in the - // BDD. That's an ambiguous specialization, as described above. - if !greatest_lower_bound.is_constraint_set_assignable_to(db, least_upper_bound) { - tracing::trace!( - target: "ty_python_semantic::types::constraints::specialize_constrained", - bound_typevar = %identity.display(db), - greatest_lower_bound = %greatest_lower_bound.display(db), - least_upper_bound = %least_upper_bound.display(db), - "typevar bounds are incompatible", - ); - error_occurred = true; - return None; - } - - // Of all of the types that satisfy all of the paths in the BDD, we choose the - // "largest" one (i.e., "closest to `object`") as the specialization. - tracing::trace!( - target: "ty_python_semantic::types::constraints::specialize_constrained", - bound_typevar = %identity.display(db), - specialization = %least_upper_bound.display(db), - "found specialization for typevar", - ); - Some(least_upper_bound) - }); - - let specialization = self.specialize_recursive(db, types); - if error_occurred { - return Err(()); - } - Ok(specialization) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 9f91105d50cec..2847d135dc5c0 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1082,9 +1082,6 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { return f .write_str("bound method `ConstraintSet.satisfied_by_all_typevars`"); } - KnownBoundMethodType::GenericContextSpecializeConstrained(_) => { - return f.write_str("bound method `GenericContext.specialize_constrained`"); - } }; let class_ty = cls.to_class_literal(self.db); diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 800850f113664..1fb55e591dad9 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -146,14 +146,6 @@ class GenericContext: alias. """ - def specialize_constrained( - self, constraints: ConstraintSet - ) -> Specialization | None: - """ - Returns a specialization of this generic context that satisfies the - given constraints, or None if the constraints cannot be satisfied. - """ - class Specialization: """A mapping of typevars to specific types""" From 968a555d65c1a92b453a2947c085e5ae4d976c7f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Mar 2026 16:01:10 +0000 Subject: [PATCH 168/261] [ty] Add unbound type variable detection in annotations (#23641) Co-authored-by: Douglas Creager --- crates/ty/docs/rules.md | 237 ++++++++++-------- .../annotations/unsupported_special_forms.md | 1 + .../mdtest/generics/pep695/aliases.md | 1 + .../resources/mdtest/generics/scoping.md | 54 +++- .../resources/mdtest/implicit_type_aliases.md | 7 + .../resources/mdtest/protocols.md | 2 + .../src/types/diagnostic.rs | 31 +++ .../ty_python_semantic/src/types/generics.rs | 26 +- .../src/types/infer/builder.rs | 10 + .../infer/builder/annotation_expression.rs | 32 +-- .../types/infer/builder/type_expression.rs | 194 ++++++++------ ty.schema.json | 10 + 12 files changed, 402 insertions(+), 203 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 1788d7b7558a1..00eb3841c08ac 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -640,7 +640,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -672,7 +672,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -798,7 +798,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -828,7 +828,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -854,7 +854,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -888,7 +888,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -977,7 +977,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1004,7 +1004,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1032,7 +1032,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1066,7 +1066,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1102,7 +1102,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1126,7 +1126,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1153,7 +1153,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1190,7 +1190,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1222,7 +1222,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1251,7 +1251,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1295,7 +1295,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1337,7 +1337,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1381,7 +1381,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1419,7 +1419,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1498,7 +1498,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1537,7 +1537,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1598,7 +1598,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1633,7 +1633,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1661,7 +1661,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1695,7 +1695,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1802,7 +1802,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1856,7 +1856,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1886,7 +1886,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1936,7 +1936,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1962,7 +1962,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1993,7 +1993,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2027,7 +2027,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2076,7 +2076,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2105,7 +2105,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2201,7 +2201,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2247,7 +2247,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2274,7 +2274,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2321,7 +2321,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2351,7 +2351,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2381,7 +2381,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2415,7 +2415,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2449,7 +2449,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2480,7 +2480,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2527,7 +2527,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2559,7 +2559,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2594,7 +2594,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2625,7 +2625,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2680,7 +2680,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2723,7 +2723,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2748,7 +2748,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2781,7 +2781,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2810,7 +2810,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2836,7 +2836,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2860,7 +2860,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2893,7 +2893,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2926,7 +2926,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2953,7 +2953,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2980,7 +2980,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3008,7 +3008,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3040,7 +3040,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3077,7 +3077,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3141,7 +3141,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3168,7 +3168,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3200,7 +3200,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3234,7 +3234,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3264,7 +3264,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3293,7 +3293,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3327,7 +3327,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3354,7 +3354,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3382,7 +3382,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3422,13 +3422,50 @@ class A: - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) +## `unbound-type-variable` + + +Default level: error · +Added in 0.0.20 · +Related issues · +View source + + + +**What it does** + +Checks for type variables that are used in a scope where they are not bound +to any enclosing generic context. + +**Why is this bad?** + +Using a type variable outside of a scope that binds it has no well-defined meaning. + +**Examples** + +```python +from typing import TypeVar, Generic + +T = TypeVar("T") +S = TypeVar("S") + +x: T # error: unbound type variable in module scope + +class C(Generic[T]): + x: list[S] = [] # error: S is not in this class's generic context +``` + +**References** + +- [Typing spec: Scoping rules for type variables](https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables) + ## `undefined-reveal` Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3452,7 +3489,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3479,7 +3516,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3507,7 +3544,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3565,7 +3602,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3590,7 +3627,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3615,7 +3652,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3654,7 +3691,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3691,7 +3728,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3732,7 +3769,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3833,7 +3870,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3896,7 +3933,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 20a6240e4d5d2..786c614fb6a56 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -109,6 +109,7 @@ from typing_extensions import Self, TypeAlias, TypeVar T = TypeVar("T") # error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter" +# error: [unbound-type-variable] X: TypeAlias[T] = int class Foo[T]: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 033452937f671..5a3140926d70d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -132,6 +132,7 @@ def _(x: ProtoInt[int]): # TODO: TypedDict is just a function object at runtime, we should emit an error class LegacyDict(TypedDict[T]): + # error: [unbound-type-variable] x: T type LegacyDictInt = LegacyDict[int] diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index 21eca20740bd0..03e007681916d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -17,15 +17,15 @@ from typing import TypeVar T = TypeVar("T") -# TODO: error +# error: [unbound-type-variable] x: T class C: - # TODO: error + # error: [unbound-type-variable] x: T def f() -> None: - # TODO: error + # error: [unbound-type-variable] x: T ``` @@ -186,11 +186,11 @@ S = TypeVar("S") def f(x: T) -> None: x: list[T] = [] - # TODO: invalid-assignment error + # error: [unbound-type-variable] y: list[S] = [] class C(Generic[T]): - # TODO: error: cannot use S if it's not in the current generic context + # error: [unbound-type-variable] x: list[S] = [] # This is not an error, as shown in the previous test @@ -210,11 +210,11 @@ S = TypeVar("S") def f[T](x: T) -> None: x: list[T] = [] - # TODO: invalid assignment error + # error: [unbound-type-variable] y: list[S] = [] class C[T]: - # TODO: error: cannot use S if it's not in the current generic context + # error: [unbound-type-variable] x: list[S] = [] def m1(self, x: S) -> S: @@ -224,6 +224,44 @@ class C[T]: return x ``` +## Should `Callable` annotations create an implicit generic context? + +There is disagreement among type checkers around how to handle this case. For now, we do not emit an +error on the following snippet, but we may change this in the future. + +```py +from typing import TypeVar, Callable +from ty_extensions import generic_context + +T = TypeVar("T") + +x: Callable[[T], T] = lambda obj: obj + +# TODO: if we decide that `Callable` annotations always create an implicit generic context, +# all of these revealed types and `invalid-argument-type` diagnostics are incorrect. +# If we decide that they do not, we should emit `unbound-type-variable` on both the +# declaration of `x` in the global scope and the parameter annotation of `y`. +# +# NOTE: all the `reveal_type`s are inside a function here so that we test the behaviour +# of the declared type (from the annotation) rather than the local inferred type +def test(y: Callable[[T], T]): + # revealed: None + reveal_type(generic_context(x)) + # revealed: (TypeVar, /) -> TypeVar + reveal_type(x) + # error: [invalid-argument-type] + # revealed: TypeVar + reveal_type(x(42)) + + # revealed: None + reveal_type(generic_context(y)) + # revealed: (T@test, /) -> T@test + reveal_type(y) + # error: [invalid-argument-type] + # revealed: T@test + reveal_type(y(42)) +``` + ## Nested formal typevars must be distinct Generic functions and classes can be nested in each other, but it is an error for the same typevar @@ -365,7 +403,7 @@ class C[T]: ok1: list[T] = [] class Bad: - # TODO: error: cannot refer to T in nested scope + # error: [unbound-type-variable] bad: list[T] = [] class Inner[S]: ... diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 3d687c73bd74a..51eb51d0d8a54 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -709,6 +709,7 @@ def _(doubly_specialized: ProtoInt[int]): # TODO: TypedDict is just a function object at runtime, we should emit an error class LegacyDict(TypedDict[T]): + # error: [unbound-type-variable] x: T # TODO: should be a `not-subscriptable` error @@ -784,7 +785,13 @@ def _( Similarly, if you try to specialize a union type without a binding context, we emit an error: ```py +from typing import TypeVar + +T = TypeVar("T") + # error: [not-subscriptable] "Cannot subscript non-generic type" +# error: [unbound-type-variable] +# error: [unbound-type-variable] x: (list[T] | set[T])[int] def _(): diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index cee9b8eec3c79..0dd52379f8de1 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -3208,6 +3208,8 @@ S = TypeVar("S") class Bar(Protocol[S]): def x(self) -> "S | Bar[S]": ... +# error: [unbound-type-variable] +# error: [unbound-type-variable] z: S | Bar[S] ``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index debd3775a7c91..ae3ee181c4cc8 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -106,6 +106,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS); registry.register_lint(&INVALID_TYPE_VARIABLE_BOUND); registry.register_lint(&INVALID_TYPE_VARIABLE_DEFAULT); + registry.register_lint(&UNBOUND_TYPE_VARIABLE); registry.register_lint(&MISSING_ARGUMENT); registry.register_lint(&NO_MATCHING_OVERLOAD); registry.register_lint(&NOT_SUBSCRIPTABLE); @@ -1808,6 +1809,36 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for type variables that are used in a scope where they are not bound + /// to any enclosing generic context. + /// + /// ## Why is this bad? + /// Using a type variable outside of a scope that binds it has no well-defined meaning. + /// + /// ## Examples + /// ```python + /// from typing import TypeVar, Generic + /// + /// T = TypeVar("T") + /// S = TypeVar("S") + /// + /// x: T # error: unbound type variable in module scope + /// + /// class C(Generic[T]): + /// x: list[S] = [] # error: S is not in this class's generic context + /// ``` + /// + /// ## References + /// - [Typing spec: Scoping rules for type variables](https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables) + pub(crate) static UNBOUND_TYPE_VARIABLE = { + summary: "detects type variables used outside of their bound scope", + status: LintStatus::stable("0.0.20"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for missing required arguments in a call. diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 4bde75f353e4e..fd813063e15bc 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -95,13 +95,25 @@ pub(crate) fn bind_typevar<'db>( return Some(typevar.with_binding_context(db, definition)); } } - enclosing_generic_contexts(db, index, containing_scope) - .find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar)) - .or_else(|| { - typevar_binding_context.map(|typevar_binding_context| { - typevar.with_binding_context(db, typevar_binding_context) - }) - }) + // Walk ancestor scopes, tracking whether we've crossed a class scope boundary. + // Class-scoped type variables are not visible from inner class scopes. + let mut crossed_class_scope = false; + for (_, ancestor_scope) in index.ancestor_scopes(containing_scope) { + let is_class_scope = ancestor_scope.kind().is_class(); + // If we've already crossed a class boundary, skip class-scoped generic contexts. + // This prevents inner classes from accessing type parameters of outer classes. + if (!is_class_scope || !crossed_class_scope) + && let Some(generic_context) = ancestor_scope.node().generic_context(db, index) + && let Some(bound) = generic_context.binds_typevar(db, typevar) + { + return Some(bound); + } + if is_class_scope { + crossed_class_scope = true; + } + } + typevar_binding_context + .map(|typevar_binding_context| typevar.with_binding_context(db, typevar_binding_context)) } /// Create a `typing.Self` type variable for a given class. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6754de4c9538c..3769122e88dea 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -297,6 +297,12 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { /// Whether we are in a context that binds unbound typevars. typevar_binding_context: Option>, + /// Whether to check for unbound type variables in type expressions. + /// This is set to `true` when processing annotation expressions, where unbound type variables + /// are an error. It is `false` in other contexts (e.g., `TypeVar` defaults, explicit class + /// specialization) where unbound type variables are expected. + check_unbound_typevars: bool, + /// The deferred state of inferring types of certain expressions within the region. /// /// This is different from [`InferenceRegion::Deferred`] which works on the entire definition @@ -363,6 +369,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bindings: VecMap::default(), declarations: VecMap::default(), typevar_binding_context: None, + check_unbound_typevars: false, deferred: VecSet::default(), undecorated_type: None, cycle_recovery: None, @@ -14603,6 +14610,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // builder only state typevar_binding_context: _, + check_unbound_typevars: _, deferred_state: _, multi_inference_state: _, inner_expression_inference_state: _, @@ -14672,6 +14680,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { dataclass_field_specifiers: _, all_definitely_bound: _, typevar_binding_context: _, + check_unbound_typevars: _, deferred_state: _, inferring_vararg_annotation: _, multi_inference_state: _, @@ -14754,6 +14763,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { dataclass_field_specifiers: _, all_definitely_bound: _, typevar_binding_context: _, + check_unbound_typevars: _, deferred_state: _, multi_inference_state: _, inner_expression_inference_state: _, diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 9d3a807a36b5b..148778d1272ab 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -71,7 +71,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state); + let previous_check_unbound_typevars = + std::mem::replace(&mut self.check_unbound_typevars, true); let annotation_ty = self.infer_annotation_expression_impl(annotation, pep_613_policy); + self.check_unbound_typevars = previous_check_unbound_typevars; self.deferred_state = previous_deferred_state; annotation_ty } @@ -134,21 +137,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; special_case.unwrap_or_else(|| { - TypeAndQualifiers::declared( - ty.default_specialize(builder.db()) - .in_type_expression( - builder.db(), - builder.scope(), - builder.typevar_binding_context, + let result_ty = ty + .default_specialize(builder.db()) + .in_type_expression( + builder.db(), + builder.scope(), + builder.typevar_binding_context, + ) + .unwrap_or_else(|error| { + error.into_fallback_type( + &builder.context, + annotation, + builder.is_reachable(annotation), ) - .unwrap_or_else(|error| { - error.into_fallback_type( - &builder.context, - annotation, - builder.is_reachable(annotation), - ) - }), - ) + }); + let result_ty = builder.check_for_unbound_type_variable(annotation, result_ty); + TypeAndQualifiers::declared(result_ty) }) } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index e593b3ea275e0..f55511381ada3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -5,8 +5,8 @@ use super::{DeferredExpressionState, TypeInferenceBuilder}; use crate::FxOrderSet; use crate::semantic_index::semantic_index; use crate::types::diagnostic::{ - self, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, - report_invalid_arguments_to_callable, + self, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, UNBOUND_TYPE_VARIABLE, + report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable, }; use crate::types::generics::bind_typevar; use crate::types::infer::builder::InnerExpressionInferenceState; @@ -82,17 +82,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression match expression { ast::Expr::Name(name) => match name.ctx { - ast::ExprContext::Load => self - .infer_name_expression(name) - .default_specialize(self.db()) - .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) - .unwrap_or_else(|error| { - error.into_fallback_type( - &self.context, - expression, - self.is_reachable(expression), - ) - }), + ast::ExprContext::Load => { + let ty = self + .infer_name_expression(name) + .default_specialize(self.db()) + .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) + .unwrap_or_else(|error| { + error.into_fallback_type( + &self.context, + expression, + self.is_reachable(expression), + ) + }); + self.check_for_unbound_type_variable(expression, ty) + } ast::ExprContext::Invalid => Type::unknown(), ast::ExprContext::Store | ast::ExprContext::Del => { todo_type!("Name expression annotation in Store/Del context") @@ -1285,81 +1288,99 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// Infer the type of a `Callable[...]` type expression. pub(crate) fn infer_callable_type(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { - let db = self.db(); + fn inner<'db>( + builder: &mut TypeInferenceBuilder<'db, '_>, + subscript: &ast::ExprSubscript, + ) -> Type<'db> { + let db = builder.db(); - let arguments_slice = &*subscript.slice; + let arguments_slice = &*subscript.slice; - let mut arguments = match arguments_slice { - ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), - _ => { - self.infer_callable_parameter_types(arguments_slice); - Either::Right(std::iter::empty::<&ast::Expr>()) - } - }; + let mut arguments = match arguments_slice { + ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), + _ => { + builder.infer_callable_parameter_types(arguments_slice); + Either::Right(std::iter::empty::<&ast::Expr>()) + } + }; - let first_argument = arguments.next(); + let first_argument = arguments.next(); - let parameters = first_argument.and_then(|arg| self.infer_callable_parameter_types(arg)); + let parameters = + first_argument.and_then(|arg| builder.infer_callable_parameter_types(arg)); - let return_type = arguments.next().map(|arg| self.infer_type_expression(arg)); + let return_type = arguments + .next() + .map(|arg| builder.infer_type_expression(arg)); - let callable_type = if parameters.is_none() - && let Some(first_argument) = first_argument - && let ast::Expr::List(list) = first_argument - && let [single_param] = &list.elts[..] - && single_param.is_ellipsis_literal_expr() - { - self.store_expression_type(single_param, Type::unknown()); - if let Some(mut diagnostic) = self.report_invalid_type_expression( - first_argument, - "`[...]` is not a valid parameter list for `Callable`", - ) { - if let Some(returns) = return_type { - diagnostic.set_primary_message(format_args!( - "Did you mean `Callable[..., {}]`?", - returns.display(db) - )); - } - } - Type::single_callable( - db, - Signature::new( - Parameters::unknown(), - return_type.unwrap_or_else(Type::unknown), - ), - ) - } else { - let correct_argument_number = if let Some(third_argument) = arguments.next() { - self.infer_type_expression(third_argument); - for argument in arguments { - self.infer_type_expression(argument); + let callable_type = if parameters.is_none() + && let Some(first_argument) = first_argument + && let ast::Expr::List(list) = first_argument + && let [single_param] = &list.elts[..] + && single_param.is_ellipsis_literal_expr() + { + builder.store_expression_type(single_param, Type::unknown()); + if let Some(mut diagnostic) = builder.report_invalid_type_expression( + first_argument, + "`[...]` is not a valid parameter list for `Callable`", + ) { + if let Some(returns) = return_type { + diagnostic.set_primary_message(format_args!( + "Did you mean `Callable[..., {}]`?", + returns.display(db) + )); + } } - false + Type::single_callable( + db, + Signature::new( + Parameters::unknown(), + return_type.unwrap_or_else(Type::unknown), + ), + ) } else { - return_type.is_some() - }; + let correct_argument_number = if let Some(third_argument) = arguments.next() { + builder.infer_type_expression(third_argument); + for argument in arguments { + builder.infer_type_expression(argument); + } + false + } else { + return_type.is_some() + }; - if !correct_argument_number { - report_invalid_arguments_to_callable(&self.context, subscript); - } + if !correct_argument_number { + report_invalid_arguments_to_callable(&builder.context, subscript); + } - if correct_argument_number - && let (Some(parameters), Some(return_type)) = (parameters, return_type) - { - Type::single_callable(db, Signature::new(parameters, return_type)) - } else { - Type::Callable(CallableType::unknown(db)) + if correct_argument_number + && let (Some(parameters), Some(return_type)) = (parameters, return_type) + { + Type::single_callable(db, Signature::new(parameters, return_type)) + } else { + Type::Callable(CallableType::unknown(db)) + } + }; + + // `Signature` / `Parameters` are not a `Type` variant, so we're storing + // the outer callable type on these expressions instead. + builder.store_expression_type(arguments_slice, callable_type); + if let Some(first_argument) = first_argument { + builder.store_expression_type(first_argument, callable_type); } - }; - // `Signature` / `Parameters` are not a `Type` variant, so we're storing - // the outer callable type on these expressions instead. - self.store_expression_type(arguments_slice, callable_type); - if let Some(first_argument) = first_argument { - self.store_expression_type(first_argument, callable_type); + callable_type } - callable_type + // There is disagreement among type checkers about whether `Callable` annotations + // in the global scope or similar should be considered to create an implicit generic context. + // For now, we do not report unbound type variables in any `Callable` contexts, but we may + // decide to revisit this in the future. + let previous_check_unbound_typevars = + std::mem::replace(&mut self.check_unbound_typevars, false); + let result = inner(self, subscript); + self.check_unbound_typevars = previous_check_unbound_typevars; + result } fn infer_parameterized_special_form_type_expression( @@ -1989,4 +2010,29 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } None } + + /// Checks if the inferred type is an unbound type variable and reports a diagnostic if so. + /// + /// Returns `Unknown` as a fallback if the type variable is unbound, otherwise returns the + /// original type unchanged. + pub(super) fn check_for_unbound_type_variable( + &self, + expression: &ast::Expr, + ty: Type<'db>, + ) -> Type<'db> { + if !self.check_unbound_typevars { + return ty; + } + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty { + if let Some(builder) = self.context.report_lint(&UNBOUND_TYPE_VARIABLE, expression) { + builder.into_diagnostic(format_args!( + "Type variable `{name}` is not bound to any outer generic context", + name = typevar.name(self.db()) + )); + } + Type::unknown() + } else { + ty + } + } } diff --git a/ty.schema.json b/ty.schema.json index 67193ebf9b9d1..f3204fc366ccc 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1325,6 +1325,16 @@ } ] }, + "unbound-type-variable": { + "title": "detects type variables used outside of their bound scope", + "description": "## What it does\nChecks for type variables that are used in a scope where they are not bound\nto any enclosing generic context.\n\n## Why is this bad?\nUsing a type variable outside of a scope that binds it has no well-defined meaning.\n\n## Examples\n```python\nfrom typing import TypeVar, Generic\n\nT = TypeVar(\"T\")\nS = TypeVar(\"S\")\n\nx: T # error: unbound type variable in module scope\n\nclass C(Generic[T]):\n x: list[S] = [] # error: S is not in this class's generic context\n```\n\n## References\n- [Typing spec: Scoping rules for type variables](https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "undefined-reveal": { "title": "detects usages of `reveal_type` without importing it", "description": "## What it does\nChecks for calls to `reveal_type` without importing it.\n\n## Why is this bad?\nUsing `reveal_type` without importing it will raise a `NameError` at runtime.\n\n## Examples\n```python\nreveal_type(1) # NameError: name 'reveal_type' is not defined\n```", From 347452f41f5abe651362e936255551a32f45419d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Mar 2026 16:01:34 +0000 Subject: [PATCH 169/261] [ty] Move `UnionType` and `IntersectionType` to a new `types::set_theoretic` submodule (#23678) --- crates/ty_python_semantic/src/types.rs | 833 +---------------- .../src/types/infer/builder.rs | 2 +- .../ty_python_semantic/src/types/relation.rs | 2 +- .../src/types/set_theoretic.rs | 854 ++++++++++++++++++ .../src/types/{ => set_theoretic}/builder.rs | 20 +- crates/ty_python_semantic/src/types/tuple.rs | 2 +- 6 files changed, 864 insertions(+), 849 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/set_theoretic.rs rename crates/ty_python_semantic/src/types/{ => set_theoretic}/builder.rs (99%) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a235186834af9..7d33783a9ff6a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -22,7 +22,6 @@ use ruff_text_size::{Ranged, TextRange}; use smallvec::{SmallVec, smallvec_inline}; use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; -pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder}; pub(crate) use self::class::DynamicClassLiteral; pub use self::cyclic::CycleDetector; pub(crate) use self::cyclic::{PairVisitor, TypeTransformer}; @@ -32,6 +31,11 @@ pub(crate) use self::infer::{ TypeContext, infer_complete_scope_types, infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, infer_scope_types, }; +pub(crate) use self::set_theoretic::builder::{IntersectionBuilder, UnionBuilder}; +pub use self::set_theoretic::{ + IntersectionType, NegativeIntersectionElements, NegativeIntersectionElementsIterator, UnionType, +}; +use self::set_theoretic::{KnownUnion, walk_intersection_type, walk_union}; pub use self::signatures::ParameterKind; pub(crate) use self::signatures::{CallableSignature, Signature}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; @@ -46,7 +50,6 @@ use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{imported_modules, place_table, semantic_index}; use crate::suppression::check_suppressions; use crate::types::bound_super::BoundSuperType; -use crate::types::builder::RecursivelyDefined; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; use crate::types::class::NamedTupleSpec; pub(crate) use crate::types::class_base::ClassBase; @@ -92,7 +95,6 @@ pub(crate) use literal::{ pub use special_form::SpecialFormType; mod bound_super; -mod builder; mod call; mod class; mod class_base; @@ -116,6 +118,7 @@ mod newtype; mod overrides; mod protocol_class; pub(crate) mod relation; +mod set_theoretic; mod signatures; mod special_form; mod string_annotation; @@ -11696,830 +11699,6 @@ pub(super) struct MetaclassTransformInfo<'db> { pub(super) from_explicit_metaclass: bool, } -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct UnionType<'db> { - /// The union type includes values in any of these types. - #[returns(deref)] - pub elements: Box<[Type<'db>]>, - /// Whether the value pointed to by this type is recursively defined. - /// If `Yes`, union literal widening is performed early. - recursively_defined: RecursivelyDefined, -} - -pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - union: UnionType<'db>, - visitor: &V, -) { - for element in union.elements(db) { - visitor.visit_type(db, *element); - } -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for UnionType<'_> {} - -#[salsa::tracked] -impl<'db> UnionType<'db> { - /// Create a union from a list of elements - /// (which may be eagerly simplified into a different variant of [`Type`] altogether). - /// - /// For performance reasons, consider using [`UnionType::from_two_elements`] if - /// the union is constructed from exactly two elements. - pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - elements - .into_iter() - .fold(UnionBuilder::new(db), |builder, element| { - builder.add(element.into()) - }) - .build() - } - - /// Create a union type `A | B` from two elements `A` and `B`. - #[salsa::tracked( - cycle_initial=|_, id, _, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { - result.cycle_normalized(db, *previous, cycle) - }, - heap_size=ruff_memory_usage::heap_size - )] - pub fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { - UnionBuilder::new(db).add(a).add(b).build() - } - - /// Create a union from a list of elements without unpacking type aliases. - pub(crate) fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - elements - .into_iter() - .fold( - UnionBuilder::new(db).unpack_aliases(false), - |builder, element| builder.add(element.into()), - ) - .build() - } - - fn from_elements_cycle_recovery(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - elements - .into_iter() - .fold( - UnionBuilder::new(db).cycle_recovery(true), - |builder, element| builder.add(element.into()), - ) - .build() - } - - /// A fallible version of [`UnionType::from_elements`]. - /// - /// If all items in `elements` are `Some()`, the result of unioning all elements is returned. - /// As soon as a `None` element in the iterable is encountered, - /// the function short-circuits and returns `None`. - pub(crate) fn try_from_elements(db: &'db dyn Db, elements: I) -> Option> - where - I: IntoIterator>, - T: Into>, - { - let mut builder = UnionBuilder::new(db); - for element in elements { - builder = builder.add(element?.into()); - } - Some(builder.build()) - } - - /// Apply a transformation function to all elements of the union, - /// and create a new union from the resulting set of types. - pub(crate) fn map( - self, - db: &'db dyn Db, - transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, - ) -> Type<'db> { - self.elements(db) - .iter() - .map(transform_fn) - .fold(UnionBuilder::new(db), |builder, element| { - builder.add(element) - }) - .recursively_defined(self.recursively_defined(db)) - .build() - } - - /// A version of [`UnionType::map`] that does not unpack type aliases. - pub(crate) fn map_leave_aliases( - self, - db: &'db dyn Db, - transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, - ) -> Type<'db> { - self.elements(db) - .iter() - .map(transform_fn) - .fold( - UnionBuilder::new(db).unpack_aliases(false), - UnionBuilder::add, - ) - .recursively_defined(self.recursively_defined(db)) - .build() - } - - /// A fallible version of [`UnionType::map`]. - /// - /// For each element in `self`, `transform_fn` is called on that element. - /// If `transform_fn` returns `Some()` for all elements in `self`, - /// the result of unioning all transformed elements is returned. - /// As soon as `transform_fn` returns `None` for an element, however, - /// the function short-circuits and returns `None`. - pub(crate) fn try_map( - self, - db: &'db dyn Db, - transform_fn: impl FnMut(&Type<'db>) -> Option>, - ) -> Option> { - let mut builder = UnionBuilder::new(db); - for element in self.elements(db).iter().map(transform_fn) { - builder = builder.add(element?); - } - builder = builder.recursively_defined(self.recursively_defined(db)); - Some(builder.build()) - } - - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> { - self.try_map(db, |element| element.to_instance(db)) - } - - pub(crate) fn filter(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { - let current = self.elements(db); - let new: Box<[Type<'db>]> = current.iter().copied().filter(f).collect(); - match &*new { - [] => Type::Never, - [single] => *single, - _ if new.len() == current.len() => Type::Union(self), - _ => Type::Union(UnionType::new(db, new, self.recursively_defined(db))), - } - } - - pub(crate) fn map_with_boundness( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, - ) -> Place<'db> { - let mut builder = UnionBuilder::new(db); - - let mut all_unbound = true; - let mut possibly_unbound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.elements(db) { - let ty_member = transform_fn(ty); - match ty_member { - Place::Undefined => { - possibly_unbound = true; - } - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - if member_boundness == Definedness::PossiblyUndefined { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - - if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder - .recursively_defined(self.recursively_defined(db)) - .build(), - origin, - definedness: if possibly_unbound { - Definedness::PossiblyUndefined - } else { - Definedness::AlwaysDefined - }, - widening: Widening::None, - }) - } - } - - pub(crate) fn map_with_boundness_and_qualifiers( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, - ) -> PlaceAndQualifiers<'db> { - let mut builder = UnionBuilder::new(db); - let mut qualifiers = TypeQualifiers::empty(); - - let mut all_unbound = true; - let mut possibly_unbound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.elements(db) { - let PlaceAndQualifiers { - place: ty_member, - qualifiers: new_qualifiers, - } = transform_fn(ty); - qualifiers |= new_qualifiers; - match ty_member { - Place::Undefined => { - possibly_unbound = true; - } - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - if member_boundness == Definedness::PossiblyUndefined { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - PlaceAndQualifiers { - place: if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder - .recursively_defined(self.recursively_defined(db)) - .build(), - origin, - definedness: if possibly_unbound { - Definedness::PossiblyUndefined - } else { - Definedness::AlwaysDefined - }, - widening: Widening::None, - }) - }, - qualifiers, - } - } - - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option> { - let mut builder = UnionBuilder::new(db) - .unpack_aliases(false) - .cycle_recovery(true) - .recursively_defined(self.recursively_defined(db)); - let mut empty = true; - for ty in self.elements(db) { - if nested { - // list[T | Divergent] => list[Divergent] - let ty = ty.recursive_type_normalized_impl(db, div, nested)?; - if ty == div { - return Some(ty); - } - builder = builder.add(ty); - empty = false; - } else { - // `Divergent` in a union type does not mean true divergence, so we skip it if not nested. - // e.g. T | Divergent == T | (T | (T | (T | ...))) == T - if ty == &div { - builder = builder.recursively_defined(RecursivelyDefined::Yes); - continue; - } - builder = builder.add( - ty.recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div), - ); - empty = false; - } - } - if empty { - builder = builder.add(div); - } - Some(builder.build()) - } - - /// Identify some specific unions of known classes, currently the ones that `float` and - /// `complex` expand into in type position. - pub(crate) fn known(self, db: &'db dyn Db) -> Option { - let mut has_int = false; - let mut has_float = false; - let mut has_complex = false; - for element in self.elements(db) { - match element.as_nominal_instance()?.known_class(db)? { - KnownClass::Int => has_int = true, - KnownClass::Float => has_float = true, - KnownClass::Complex => has_complex = true, - _ => return None, - } - } - match (has_int, has_float, has_complex) { - (true, true, false) => Some(KnownUnion::Float), - (true, true, true) => Some(KnownUnion::Complex), - _ => None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum KnownUnion { - Float, // `int | float` - Complex, // `int | float | complex` -} - -impl KnownUnion { - pub(crate) fn to_type(self, db: &dyn Db) -> Type<'_> { - match self { - KnownUnion::Float => UnionType::from_two_elements( - db, - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - ), - KnownUnion::Complex => UnionType::from_elements( - db, - [ - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - KnownClass::Complex.to_instance(db), - ], - ), - } - } -} - -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct IntersectionType<'db> { - /// The intersection type includes only values in all of these types. - #[returns(ref)] - positive: FxOrderSet>, - - /// The intersection type does not include any value in any of these types. - /// - /// Negation types aren't expressible in annotations, and are most likely to arise from type - /// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them - /// directly in intersections rather than as a separate type. - #[returns(ref)] - negative: NegativeIntersectionElements<'db>, -} - -/// To avoid unnecessary allocations for the common case of 1 negative elements, -/// we use this enum to represent the negative elements of an intersection type. -/// -/// It should otherwise have identical behavior to `FxOrderSet>`. -/// -/// Note that we do not try to maintain the invariant that length-0 collections -/// are always represented using `Self::Empty`, and that length-1 collections -/// are always represented using `Self::Single`: `Self::Multiple` is permitted -/// to have 0-1 elements in its wrapped data, and this could happen if you called -/// `Self::swap_remove` or `Self::swap_remove_index` on an instance that is -/// already the `Self::Multiple` variant. Maintaining the invariant that -/// 0-length or 1-length collections are always represented using `Self::Empty` -/// and `Self::Single` would add overhead to methods like `Self::swap_remove`, -/// and would have little value. At the point when you're calling that method, a -/// heap allocation has already taken place. -#[derive(Debug, Clone, get_size2::GetSize, salsa::Update, Default)] -pub enum NegativeIntersectionElements<'db> { - #[default] - Empty, - Single(Type<'db>), - Multiple(FxOrderSet>), -} - -impl<'db> NegativeIntersectionElements<'db> { - pub(crate) fn iter(&self) -> NegativeIntersectionElementsIterator<'_, 'db> { - match self { - Self::Empty => NegativeIntersectionElementsIterator::EmptyOrOne(None), - Self::Single(ty) => NegativeIntersectionElementsIterator::EmptyOrOne(Some(ty)), - Self::Multiple(set) => NegativeIntersectionElementsIterator::Multiple(set.iter()), - } - } - - pub(crate) fn len(&self) -> usize { - match self { - Self::Empty => 0, - Self::Single(_) => 1, - Self::Multiple(set) => set.len(), - } - } - - pub(crate) fn contains(&self, ty: &Type<'db>) -> bool { - match self { - Self::Empty => false, - Self::Single(existing) => existing == ty, - Self::Multiple(set) => set.contains(ty), - } - } - - pub(crate) fn is_empty(&self) -> bool { - // See struct-level comment: we don't try to maintain the invariant that empty - // collections are representend as `Self::Empty` - self.len() == 0 - } - - /// Insert the type into the collection. - /// - /// Returns `true` if the elements was newly added. - /// Returns `false` if the element was already present in the collection. - pub(crate) fn insert(&mut self, ty: Type<'db>) -> bool { - match self { - Self::Empty => { - *self = Self::Single(ty); - true - } - Self::Single(existing) => { - if ty != *existing { - *self = Self::Multiple(FxOrderSet::from_iter([*existing, ty])); - true - } else { - false - } - } - Self::Multiple(set) => set.insert(ty), - } - } - - /// Shrink the capacity of the collection as much as possible. - pub(crate) fn shrink_to_fit(&mut self) { - match self { - Self::Empty | Self::Single(_) => {} - Self::Multiple(set) => set.shrink_to_fit(), - } - } - - /// Remove `ty` from the collection. - /// - /// Returns `true` if `ty` was previously in the collection and has now been removed. - /// Returns `false` if `ty` was never present in the collection. - /// - /// If `ty` was previously present in the collection, - /// the last element in the collection is popped off the end of the collection - /// and placed at the index where `ty` was previously, allowing this method to complete - /// in O(1) time (average). - pub(crate) fn swap_remove(&mut self, ty: &Type<'db>) -> bool { - match self { - Self::Empty => false, - Self::Single(existing) => { - if existing == ty { - *self = Self::Empty; - true - } else { - false - } - } - // See struct-level comment: we don't try to maintain the invariant that collections - // with size 0 or 1 are represented as `Empty` or `Single`. - Self::Multiple(set) => set.swap_remove(ty), - } - } - - /// Remove the element at `index` from the collection. - /// - /// The element is removed by swapping it with the last element - /// of the collection and popping it off, allowing this method to complete - /// in O(1) time (average). - pub(crate) fn swap_remove_index(&mut self, index: usize) -> Option> { - match self { - Self::Empty => None, - Self::Single(existing) => { - if index == 0 { - let ty = *existing; - *self = Self::Empty; - Some(ty) - } else { - None - } - } - // See struct-level comment: we don't try to maintain the invariant that collections - // with size 0 or 1 are represented as `Empty` or `Single`. - Self::Multiple(set) => set.swap_remove_index(index), - } - } - - /// Apply a transformation to all elements in this collection, - /// and return a new collection of the transformed elements. - fn map(&self, map_fn: impl Fn(&Type<'db>) -> Type<'db>) -> Self { - match self { - NegativeIntersectionElements::Empty => NegativeIntersectionElements::Empty, - NegativeIntersectionElements::Single(ty) => { - NegativeIntersectionElements::Single(map_fn(ty)) - } - NegativeIntersectionElements::Multiple(set) => { - NegativeIntersectionElements::Multiple(set.iter().map(map_fn).collect()) - } - } - } - - /// Apply a fallible transformation to all elements in this collection, - /// and return a new collection of the transformed elements. - /// - /// Returns `None` if `map_fn` fails for any element in the collection. - fn try_map(&self, map_fn: impl Fn(&Type<'db>) -> Option>) -> Option { - match self { - NegativeIntersectionElements::Empty => Some(NegativeIntersectionElements::Empty), - NegativeIntersectionElements::Single(ty) => { - map_fn(ty).map(NegativeIntersectionElements::Single) - } - NegativeIntersectionElements::Multiple(set) => { - Some(NegativeIntersectionElements::Multiple( - set.iter().map(map_fn).collect::>()?, - )) - } - } - } -} - -impl<'a, 'db> IntoIterator for &'a NegativeIntersectionElements<'db> { - type Item = &'a Type<'db>; - type IntoIter = NegativeIntersectionElementsIterator<'a, 'db>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl PartialEq for NegativeIntersectionElements<'_> { - fn eq(&self, other: &Self) -> bool { - // Same implementation as `OrderSet::eq` - self.len() == other.len() && self.iter().eq(other) - } -} - -impl Eq for NegativeIntersectionElements<'_> {} - -impl std::hash::Hash for NegativeIntersectionElements<'_> { - fn hash(&self, state: &mut H) { - // Same implementation as `OrderSet::hash` - self.len().hash(state); - for value in self { - value.hash(state); - } - } -} - -#[derive(Debug)] -pub enum NegativeIntersectionElementsIterator<'a, 'db> { - EmptyOrOne(Option<&'a Type<'db>>), - Multiple(ordermap::set::Iter<'a, Type<'db>>), -} - -impl<'a, 'db> Iterator for NegativeIntersectionElementsIterator<'a, 'db> { - type Item = &'a Type<'db>; - - fn next(&mut self) -> Option { - match self { - NegativeIntersectionElementsIterator::EmptyOrOne(opt) => opt.take(), - NegativeIntersectionElementsIterator::Multiple(iter) => iter.next(), - } - } -} - -impl std::iter::FusedIterator for NegativeIntersectionElementsIterator<'_, '_> {} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for IntersectionType<'_> {} - -pub(super) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - intersection: IntersectionType<'db>, - visitor: &V, -) { - for element in intersection.positive(db) { - visitor.visit_type(db, *element); - } - for element in intersection.negative(db) { - visitor.visit_type(db, *element); - } -} - -#[salsa::tracked] -impl<'db> IntersectionType<'db> { - /// Create an intersection type `E1 & E2 & ... & En` from a list of (positive) elements. - /// - /// For performance reasons, consider using [`IntersectionType::from_two_elements`] if - /// the intersection is constructed from exactly two elements. - pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - IntersectionBuilder::new(db) - .positive_elements(elements) - .build() - } - - /// Create an intersection type `A & B` from two elements `A` and `B`. - #[salsa::tracked( - cycle_initial=|_, id, _, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { - result.cycle_normalized(db, *previous, cycle) - }, - heap_size=ruff_memory_usage::heap_size - )] - fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { - IntersectionBuilder::new(db) - .positive_elements([a, b]) - .build() - } - - pub(crate) fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - let positive = if nested { - self.positive(db) - .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, nested)) - .collect::>>>()? - } else { - self.positive(db) - .iter() - .map(|ty| { - ty.recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div) - }) - .collect() - }; - - let negative = if nested { - self.negative(db) - .try_map(|ty| ty.recursive_type_normalized_impl(db, div, nested))? - } else { - self.negative(db).map(|ty| { - ty.recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div) - }) - }; - - Some(IntersectionType::new(db, positive, negative)) - } - - /// Returns an iterator over the positive elements of the intersection. If - /// there are no positive elements, returns a single `object` type. - pub(crate) fn positive_elements_or_object( - self, - db: &'db dyn Db, - ) -> impl Iterator> { - if self.positive(db).is_empty() { - Either::Left(std::iter::once(Type::object())) - } else { - Either::Right(self.positive(db).iter().copied()) - } - } - - /// Map a type transformation over all positive elements of the intersection. Leave the - /// negative elements unchanged. - pub(crate) fn map_positive( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, - ) -> Type<'db> { - let mut builder = IntersectionBuilder::new(db); - for ty in self.positive(db) { - builder = builder.add_positive(transform_fn(ty)); - } - for ty in self.negative(db) { - builder = builder.add_negative(*ty); - } - builder.build() - } - - pub(crate) fn map_with_boundness( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, - ) -> Place<'db> { - let mut builder = IntersectionBuilder::new(db); - - let mut all_unbound = true; - let mut any_definitely_bound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.positive_elements_or_object(db) { - let ty_member = transform_fn(&ty); - match ty_member { - Place::Undefined => {} - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - all_unbound = false; - if member_boundness == Definedness::AlwaysDefined { - any_definitely_bound = true; - } - - builder = builder.add_positive(ty_member); - } - } - } - - if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder.build(), - origin, - definedness: if any_definitely_bound { - Definedness::AlwaysDefined - } else { - Definedness::PossiblyUndefined - }, - widening: Widening::None, - }) - } - } - - pub(crate) fn map_with_boundness_and_qualifiers( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, - ) -> PlaceAndQualifiers<'db> { - let mut builder = IntersectionBuilder::new(db); - let mut qualifiers = TypeQualifiers::empty(); - - let mut all_unbound = true; - let mut any_definitely_bound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.positive_elements_or_object(db) { - let PlaceAndQualifiers { - place: member, - qualifiers: new_qualifiers, - } = transform_fn(&ty); - qualifiers |= new_qualifiers; - match member { - Place::Undefined => {} - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - all_unbound = false; - if member_boundness == Definedness::AlwaysDefined { - any_definitely_bound = true; - } - - builder = builder.add_positive(ty_member); - } - } - } - - PlaceAndQualifiers { - place: if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder.build(), - origin, - definedness: if any_definitely_bound { - Definedness::AlwaysDefined - } else { - Definedness::PossiblyUndefined - }, - widening: Widening::None, - }) - }, - qualifiers, - } - } - - pub fn iter_positive(self, db: &'db dyn Db) -> impl Iterator> { - self.positive(db).iter().copied() - } - - pub fn iter_negative(self, db: &'db dyn Db) -> impl Iterator> { - self.negative(db).iter().copied() - } - - pub(crate) fn has_one_element(self, db: &'db dyn Db) -> bool { - (self.positive(db).len() + self.negative(db).len()) == 1 - } - - pub(crate) fn is_simple_negation(self, db: &'db dyn Db) -> bool { - self.positive(db).is_empty() && self.negative(db).len() == 1 - } -} - #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct TypeIsType<'db> { return_type: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3769122e88dea..879992eb50006 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -59,7 +59,6 @@ use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, attribute_assignments, place_table, }; -use crate::types::builder::RecursivelyDefined; use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::class::{ @@ -120,6 +119,7 @@ use crate::types::infer::builder::paramspec_validation::validate_paramspec_compo use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function}; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; +use crate::types::set_theoretic::RecursivelyDefined; use crate::types::subclass_of::SubclassOfInner; use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType}; use crate::types::typed_dict::{ diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index da9f7a45fc5ef..f350ee1f874e8 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -3,11 +3,11 @@ use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use crate::place::{DefinedPlace, Place}; -use crate::types::builder::RecursivelyDefined; use crate::types::constraints::{ ConstraintSetBuilder, IteratorConstraintsExtension, OptionConstraintsExtension, }; use crate::types::enums::is_single_member_enum; +use crate::types::set_theoretic::RecursivelyDefined; use crate::types::{ CallableType, ClassBase, ClassType, CycleDetector, DynamicType, KnownClass, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, PairVisitor, ProtocolInstanceType, SubclassOfInner, diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs new file mode 100644 index 0000000000000..5cec8af50eae0 --- /dev/null +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -0,0 +1,854 @@ +use itertools::Either; + +use crate::place::{DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening}; +use crate::types::class::KnownClass; +use crate::types::visitor; +use crate::types::{Type, TypeQualifiers}; +use crate::{Db, FxOrderSet}; + +pub(crate) mod builder; + +pub(crate) use builder::{IntersectionBuilder, UnionBuilder}; + +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct UnionType<'db> { + /// The union type includes values in any of these types. + #[returns(deref)] + pub elements: Box<[Type<'db>]>, + /// Whether the value pointed to by this type is recursively defined. + /// If `Yes`, union literal widening is performed early. + pub(crate) recursively_defined: RecursivelyDefined, +} + +pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + union: UnionType<'db>, + visitor: &V, +) { + for element in union.elements(db) { + visitor.visit_type(db, *element); + } +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for UnionType<'_> {} + +#[salsa::tracked] +impl<'db> UnionType<'db> { + /// Create a union from a list of elements + /// (which may be eagerly simplified into a different variant of [`Type`] altogether). + /// + /// For performance reasons, consider using [`UnionType::from_two_elements`] if + /// the union is constructed from exactly two elements. + pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + elements + .into_iter() + .fold(UnionBuilder::new(db), |builder, element| { + builder.add(element.into()) + }) + .build() + } + + /// Create a union type `A | B` from two elements `A` and `B`. + #[salsa::tracked( + cycle_initial=|_, id, _, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { + result.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + pub fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + UnionBuilder::new(db).add(a).add(b).build() + } + + /// Create a union from a list of elements without unpacking type aliases. + pub(crate) fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + elements + .into_iter() + .fold( + UnionBuilder::new(db).unpack_aliases(false), + |builder, element| builder.add(element.into()), + ) + .build() + } + + pub(crate) fn from_elements_cycle_recovery(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + elements + .into_iter() + .fold( + UnionBuilder::new(db).cycle_recovery(true), + |builder, element| builder.add(element.into()), + ) + .build() + } + + /// A fallible version of [`UnionType::from_elements`]. + /// + /// If all items in `elements` are `Some()`, the result of unioning all elements is returned. + /// As soon as a `None` element in the iterable is encountered, + /// the function short-circuits and returns `None`. + pub(crate) fn try_from_elements(db: &'db dyn Db, elements: I) -> Option> + where + I: IntoIterator>, + T: Into>, + { + let mut builder = UnionBuilder::new(db); + for element in elements { + builder = builder.add(element?.into()); + } + Some(builder.build()) + } + + /// Apply a transformation function to all elements of the union, + /// and create a new union from the resulting set of types. + pub(crate) fn map( + self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, + ) -> Type<'db> { + self.elements(db) + .iter() + .map(transform_fn) + .fold(UnionBuilder::new(db), |builder, element| { + builder.add(element) + }) + .recursively_defined(self.recursively_defined(db)) + .build() + } + + /// A version of [`UnionType::map`] that does not unpack type aliases. + pub(crate) fn map_leave_aliases( + self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, + ) -> Type<'db> { + self.elements(db) + .iter() + .map(transform_fn) + .fold( + UnionBuilder::new(db).unpack_aliases(false), + UnionBuilder::add, + ) + .recursively_defined(self.recursively_defined(db)) + .build() + } + + /// A fallible version of [`UnionType::map`]. + /// + /// For each element in `self`, `transform_fn` is called on that element. + /// If `transform_fn` returns `Some()` for all elements in `self`, + /// the result of unioning all transformed elements is returned. + /// As soon as `transform_fn` returns `None` for an element, however, + /// the function short-circuits and returns `None`. + pub(crate) fn try_map( + self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Option>, + ) -> Option> { + let mut builder = UnionBuilder::new(db); + for element in self.elements(db).iter().map(transform_fn) { + builder = builder.add(element?); + } + builder = builder.recursively_defined(self.recursively_defined(db)); + Some(builder.build()) + } + + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> { + self.try_map(db, |element| element.to_instance(db)) + } + + pub(crate) fn filter(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { + let current = self.elements(db); + let new: Box<[Type<'db>]> = current.iter().copied().filter(f).collect(); + match &*new { + [] => Type::Never, + [single] => *single, + _ if new.len() == current.len() => Type::Union(self), + _ => Type::Union(UnionType::new(db, new, self.recursively_defined(db))), + } + } + + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, + ) -> Place<'db> { + let mut builder = UnionBuilder::new(db); + + let mut all_unbound = true; + let mut possibly_unbound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.elements(db) { + let ty_member = transform_fn(ty); + match ty_member { + Place::Undefined => { + possibly_unbound = true; + } + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + if member_boundness == Definedness::PossiblyUndefined { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + + if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder + .recursively_defined(self.recursively_defined(db)) + .build(), + origin, + definedness: if possibly_unbound { + Definedness::PossiblyUndefined + } else { + Definedness::AlwaysDefined + }, + widening: Widening::None, + }) + } + } + + pub(crate) fn map_with_boundness_and_qualifiers( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { + let mut builder = UnionBuilder::new(db); + let mut qualifiers = TypeQualifiers::empty(); + + let mut all_unbound = true; + let mut possibly_unbound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.elements(db) { + let PlaceAndQualifiers { + place: ty_member, + qualifiers: new_qualifiers, + } = transform_fn(ty); + qualifiers |= new_qualifiers; + match ty_member { + Place::Undefined => { + possibly_unbound = true; + } + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + if member_boundness == Definedness::PossiblyUndefined { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + PlaceAndQualifiers { + place: if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder + .recursively_defined(self.recursively_defined(db)) + .build(), + origin, + definedness: if possibly_unbound { + Definedness::PossiblyUndefined + } else { + Definedness::AlwaysDefined + }, + widening: Widening::None, + }) + }, + qualifiers, + } + } + + pub(crate) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option> { + let mut builder = UnionBuilder::new(db) + .unpack_aliases(false) + .cycle_recovery(true) + .recursively_defined(self.recursively_defined(db)); + let mut empty = true; + for ty in self.elements(db) { + if nested { + // list[T | Divergent] => list[Divergent] + let ty = ty.recursive_type_normalized_impl(db, div, nested)?; + if ty == div { + return Some(ty); + } + builder = builder.add(ty); + empty = false; + } else { + // `Divergent` in a union type does not mean true divergence, so we skip it if not nested. + // e.g. T | Divergent == T | (T | (T | (T | ...))) == T + if ty == &div { + builder = builder.recursively_defined(RecursivelyDefined::Yes); + continue; + } + builder = builder.add( + ty.recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div), + ); + empty = false; + } + } + if empty { + builder = builder.add(div); + } + Some(builder.build()) + } + + /// Identify some specific unions of known classes, currently the ones that `float` and + /// `complex` expand into in type position. + pub(crate) fn known(self, db: &'db dyn Db) -> Option { + let mut has_int = false; + let mut has_float = false; + let mut has_complex = false; + for element in self.elements(db) { + match element.as_nominal_instance()?.known_class(db)? { + KnownClass::Int => has_int = true, + KnownClass::Float => has_float = true, + KnownClass::Complex => has_complex = true, + _ => return None, + } + } + match (has_int, has_float, has_complex) { + (true, true, false) => Some(KnownUnion::Float), + (true, true, true) => Some(KnownUnion::Complex), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum KnownUnion { + Float, // `int | float` + Complex, // `int | float | complex` +} + +impl KnownUnion { + pub(crate) fn to_type(self, db: &dyn Db) -> Type<'_> { + match self { + KnownUnion::Float => UnionType::from_two_elements( + db, + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), + ), + KnownUnion::Complex => UnionType::from_elements( + db, + [ + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), + KnownClass::Complex.to_instance(db), + ], + ), + } + } +} + +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct IntersectionType<'db> { + /// The intersection type includes only values in all of these types. + #[returns(ref)] + pub(crate) positive: FxOrderSet>, + + /// The intersection type does not include any value in any of these types. + /// + /// Negation types aren't expressible in annotations, and are most likely to arise from type + /// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them + /// directly in intersections rather than as a separate type. + #[returns(ref)] + pub(crate) negative: NegativeIntersectionElements<'db>, +} + +/// To avoid unnecessary allocations for the common case of 1 negative elements, +/// we use this enum to represent the negative elements of an intersection type. +/// +/// It should otherwise have identical behavior to `FxOrderSet>`. +/// +/// Note that we do not try to maintain the invariant that length-0 collections +/// are always represented using `Self::Empty`, and that length-1 collections +/// are always represented using `Self::Single`: `Self::Multiple` is permitted +/// to have 0-1 elements in its wrapped data, and this could happen if you called +/// `Self::swap_remove` or `Self::swap_remove_index` on an instance that is +/// already the `Self::Multiple` variant. Maintaining the invariant that +/// 0-length or 1-length collections are always represented using `Self::Empty` +/// and `Self::Single` would add overhead to methods like `Self::swap_remove`, +/// and would have little value. At the point when you're calling that method, a +/// heap allocation has already taken place. +#[derive(Debug, Clone, get_size2::GetSize, salsa::Update, Default)] +pub enum NegativeIntersectionElements<'db> { + #[default] + Empty, + Single(Type<'db>), + Multiple(FxOrderSet>), +} + +impl<'db> NegativeIntersectionElements<'db> { + pub(crate) fn iter(&self) -> NegativeIntersectionElementsIterator<'_, 'db> { + match self { + Self::Empty => NegativeIntersectionElementsIterator::EmptyOrOne(None), + Self::Single(ty) => NegativeIntersectionElementsIterator::EmptyOrOne(Some(ty)), + Self::Multiple(set) => NegativeIntersectionElementsIterator::Multiple(set.iter()), + } + } + + pub(crate) fn len(&self) -> usize { + match self { + Self::Empty => 0, + Self::Single(_) => 1, + Self::Multiple(set) => set.len(), + } + } + + pub(crate) fn contains(&self, ty: &Type<'db>) -> bool { + match self { + Self::Empty => false, + Self::Single(existing) => existing == ty, + Self::Multiple(set) => set.contains(ty), + } + } + + pub(crate) fn is_empty(&self) -> bool { + // See struct-level comment: we don't try to maintain the invariant that empty + // collections are representend as `Self::Empty` + self.len() == 0 + } + + /// Insert the type into the collection. + /// + /// Returns `true` if the elements was newly added. + /// Returns `false` if the element was already present in the collection. + pub(crate) fn insert(&mut self, ty: Type<'db>) -> bool { + match self { + Self::Empty => { + *self = Self::Single(ty); + true + } + Self::Single(existing) => { + if ty != *existing { + *self = Self::Multiple(FxOrderSet::from_iter([*existing, ty])); + true + } else { + false + } + } + Self::Multiple(set) => set.insert(ty), + } + } + + /// Shrink the capacity of the collection as much as possible. + pub(crate) fn shrink_to_fit(&mut self) { + match self { + Self::Empty | Self::Single(_) => {} + Self::Multiple(set) => set.shrink_to_fit(), + } + } + + /// Remove `ty` from the collection. + /// + /// Returns `true` if `ty` was previously in the collection and has now been removed. + /// Returns `false` if `ty` was never present in the collection. + /// + /// If `ty` was previously present in the collection, + /// the last element in the collection is popped off the end of the collection + /// and placed at the index where `ty` was previously, allowing this method to complete + /// in O(1) time (average). + pub(crate) fn swap_remove(&mut self, ty: &Type<'db>) -> bool { + match self { + Self::Empty => false, + Self::Single(existing) => { + if existing == ty { + *self = Self::Empty; + true + } else { + false + } + } + // See struct-level comment: we don't try to maintain the invariant that collections + // with size 0 or 1 are represented as `Empty` or `Single`. + Self::Multiple(set) => set.swap_remove(ty), + } + } + + /// Remove the element at `index` from the collection. + /// + /// The element is removed by swapping it with the last element + /// of the collection and popping it off, allowing this method to complete + /// in O(1) time (average). + pub(crate) fn swap_remove_index(&mut self, index: usize) -> Option> { + match self { + Self::Empty => None, + Self::Single(existing) => { + if index == 0 { + let ty = *existing; + *self = Self::Empty; + Some(ty) + } else { + None + } + } + // See struct-level comment: we don't try to maintain the invariant that collections + // with size 0 or 1 are represented as `Empty` or `Single`. + Self::Multiple(set) => set.swap_remove_index(index), + } + } + + /// Apply a transformation to all elements in this collection, + /// and return a new collection of the transformed elements. + fn map(&self, map_fn: impl Fn(&Type<'db>) -> Type<'db>) -> Self { + match self { + NegativeIntersectionElements::Empty => NegativeIntersectionElements::Empty, + NegativeIntersectionElements::Single(ty) => { + NegativeIntersectionElements::Single(map_fn(ty)) + } + NegativeIntersectionElements::Multiple(set) => { + NegativeIntersectionElements::Multiple(set.iter().map(map_fn).collect()) + } + } + } + + /// Apply a fallible transformation to all elements in this collection, + /// and return a new collection of the transformed elements. + /// + /// Returns `None` if `map_fn` fails for any element in the collection. + fn try_map(&self, map_fn: impl Fn(&Type<'db>) -> Option>) -> Option { + match self { + NegativeIntersectionElements::Empty => Some(NegativeIntersectionElements::Empty), + NegativeIntersectionElements::Single(ty) => { + map_fn(ty).map(NegativeIntersectionElements::Single) + } + NegativeIntersectionElements::Multiple(set) => { + Some(NegativeIntersectionElements::Multiple( + set.iter().map(map_fn).collect::>()?, + )) + } + } + } +} + +impl<'a, 'db> IntoIterator for &'a NegativeIntersectionElements<'db> { + type Item = &'a Type<'db>; + type IntoIter = NegativeIntersectionElementsIterator<'a, 'db>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl PartialEq for NegativeIntersectionElements<'_> { + fn eq(&self, other: &Self) -> bool { + // Same implementation as `OrderSet::eq` + self.len() == other.len() && self.iter().eq(other) + } +} + +impl Eq for NegativeIntersectionElements<'_> {} + +impl std::hash::Hash for NegativeIntersectionElements<'_> { + fn hash(&self, state: &mut H) { + // Same implementation as `OrderSet::hash` + self.len().hash(state); + for value in self { + value.hash(state); + } + } +} + +#[derive(Debug)] +pub enum NegativeIntersectionElementsIterator<'a, 'db> { + EmptyOrOne(Option<&'a Type<'db>>), + Multiple(ordermap::set::Iter<'a, Type<'db>>), +} + +impl<'a, 'db> Iterator for NegativeIntersectionElementsIterator<'a, 'db> { + type Item = &'a Type<'db>; + + fn next(&mut self) -> Option { + match self { + NegativeIntersectionElementsIterator::EmptyOrOne(opt) => opt.take(), + NegativeIntersectionElementsIterator::Multiple(iter) => iter.next(), + } + } +} + +impl std::iter::FusedIterator for NegativeIntersectionElementsIterator<'_, '_> {} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for IntersectionType<'_> {} + +pub(crate) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + intersection: IntersectionType<'db>, + visitor: &V, +) { + for element in intersection.positive(db) { + visitor.visit_type(db, *element); + } + for element in intersection.negative(db) { + visitor.visit_type(db, *element); + } +} + +#[salsa::tracked] +impl<'db> IntersectionType<'db> { + /// Create an intersection type `E1 & E2 & ... & En` from a list of (positive) elements. + /// + /// For performance reasons, consider using [`IntersectionType::from_two_elements`] if + /// the intersection is constructed from exactly two elements. + pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + IntersectionBuilder::new(db) + .positive_elements(elements) + .build() + } + + /// Create an intersection type `A & B` from two elements `A` and `B`. + #[salsa::tracked( + cycle_initial=|_, id, _, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { + result.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + pub(crate) fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + IntersectionBuilder::new(db) + .positive_elements([a, b]) + .build() + } + + pub(crate) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + let positive = if nested { + self.positive(db) + .iter() + .map(|ty| ty.recursive_type_normalized_impl(db, div, nested)) + .collect::>>>()? + } else { + self.positive(db) + .iter() + .map(|ty| { + ty.recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div) + }) + .collect() + }; + + let negative = if nested { + self.negative(db) + .try_map(|ty| ty.recursive_type_normalized_impl(db, div, nested))? + } else { + self.negative(db).map(|ty| { + ty.recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div) + }) + }; + + Some(IntersectionType::new(db, positive, negative)) + } + + /// Returns an iterator over the positive elements of the intersection. If + /// there are no positive elements, returns a single `object` type. + pub(crate) fn positive_elements_or_object( + self, + db: &'db dyn Db, + ) -> impl Iterator> { + if self.positive(db).is_empty() { + Either::Left(std::iter::once(Type::object())) + } else { + Either::Right(self.positive(db).iter().copied()) + } + } + + /// Map a type transformation over all positive elements of the intersection. Leave the + /// negative elements unchanged. + pub(crate) fn map_positive( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, + ) -> Type<'db> { + let mut builder = IntersectionBuilder::new(db); + for ty in self.positive(db) { + builder = builder.add_positive(transform_fn(ty)); + } + for ty in self.negative(db) { + builder = builder.add_negative(*ty); + } + builder.build() + } + + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, + ) -> Place<'db> { + let mut builder = IntersectionBuilder::new(db); + + let mut all_unbound = true; + let mut any_definitely_bound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.positive_elements_or_object(db) { + let ty_member = transform_fn(&ty); + match ty_member { + Place::Undefined => {} + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + all_unbound = false; + if member_boundness == Definedness::AlwaysDefined { + any_definitely_bound = true; + } + + builder = builder.add_positive(ty_member); + } + } + } + + if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder.build(), + origin, + definedness: if any_definitely_bound { + Definedness::AlwaysDefined + } else { + Definedness::PossiblyUndefined + }, + widening: Widening::None, + }) + } + } + + pub(crate) fn map_with_boundness_and_qualifiers( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { + let mut builder = IntersectionBuilder::new(db); + let mut qualifiers = TypeQualifiers::empty(); + + let mut all_unbound = true; + let mut any_definitely_bound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.positive_elements_or_object(db) { + let PlaceAndQualifiers { + place: member, + qualifiers: new_qualifiers, + } = transform_fn(&ty); + qualifiers |= new_qualifiers; + match member { + Place::Undefined => {} + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + all_unbound = false; + if member_boundness == Definedness::AlwaysDefined { + any_definitely_bound = true; + } + + builder = builder.add_positive(ty_member); + } + } + } + + PlaceAndQualifiers { + place: if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder.build(), + origin, + definedness: if any_definitely_bound { + Definedness::AlwaysDefined + } else { + Definedness::PossiblyUndefined + }, + widening: Widening::None, + }) + }, + qualifiers, + } + } + + pub fn iter_positive(self, db: &'db dyn Db) -> impl Iterator> { + self.positive(db).iter().copied() + } + + pub fn iter_negative(self, db: &'db dyn Db) -> impl Iterator> { + self.negative(db).iter().copied() + } + + pub(crate) fn has_one_element(self, db: &'db dyn Db) -> bool { + (self.positive(db).len() + self.negative(db).len()) == 1 + } + + pub(crate) fn is_simple_negation(self, db: &'db dyn Db) -> bool { + self.positive(db).is_empty() && self.negative(db).len() == 1 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum RecursivelyDefined { + Yes, + No, +} + +impl RecursivelyDefined { + const fn is_yes(self) -> bool { + matches!(self, RecursivelyDefined::Yes) + } + + const fn or(self, other: RecursivelyDefined) -> RecursivelyDefined { + match (self, other) { + (RecursivelyDefined::Yes, _) | (_, RecursivelyDefined::Yes) => RecursivelyDefined::Yes, + _ => RecursivelyDefined::No, + } + } +} diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs similarity index 99% rename from crates/ty_python_semantic/src/types/builder.rs rename to crates/ty_python_semantic/src/types/set_theoretic/builder.rs index 3ae43a51d9cd2..291667e3e1145 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -36,6 +36,7 @@ //! shares exactly the same possible super-types, and none of them are subtypes of each other //! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks. +use super::RecursivelyDefined; use crate::types::enums::{enum_member_literals, enum_metadata}; use crate::types::{ BytesLiteralType, ClassLiteral, EnumLiteralType, IntersectionType, KnownClass, @@ -315,25 +316,6 @@ enum ReduceResult<'db> { Type(Type<'db>), } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] -pub enum RecursivelyDefined { - Yes, - No, -} - -impl RecursivelyDefined { - const fn is_yes(self) -> bool { - matches!(self, RecursivelyDefined::Yes) - } - - const fn or(self, other: RecursivelyDefined) -> RecursivelyDefined { - match (self, other) { - (RecursivelyDefined::Yes, _) | (_, RecursivelyDefined::Yes) => RecursivelyDefined::Yes, - _ => RecursivelyDefined::No, - } - } -} - /// If the value ​​is defined recursively, widening is performed from fewer literal elements, /// resulting in faster convergence of the fixed-point iteration. const MAX_RECURSIVE_UNION_LITERALS: usize = 5; diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index b35646e53ca60..4c602584db19d 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -24,13 +24,13 @@ use smallvec::{SmallVec, smallvec_inline}; use crate::semantic_index::definition::Definition; use crate::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; -use crate::types::builder::RecursivelyDefined; use crate::types::class::{ClassType, KnownClass}; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; use crate::types::generics::InferableTypeVars; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::set_theoretic::RecursivelyDefined; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, IntersectionType, Type, TypeMapping, UnionBuilder, UnionType, From e891d6c4bac52e2a6e01f26217e0a88959c6a71c Mon Sep 17 00:00:00 2001 From: Dex Devlon <51504045+bxff@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:36:09 +0530 Subject: [PATCH 170/261] [`ruff`] Fix false positive for `re.split` with empty string pattern (`RUF055`) (#23634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #23629. `re.split("", s)` is flagged by RUF055 and auto-fixed to `s.split("")`, but `str.split("")` raises `ValueError: empty separator` while `re.split("", s)` succeeds (returning `["", "a", "b", "c", ""]`). The same applies to bytes (`rb""`). This adds a guard to skip the diagnostic when the separator pattern is an empty string or bytes literal specifically for `re.split` calls. Other `re` functions (`sub`, `match`, `search`, `fullmatch`) are not affected — their `str` equivalents all handle empty strings equivalently. ## Test Plan Added test cases for empty string and bytes patterns in `RUF055_0.py` and `RUF055_3.py`. Verified that no diagnostics are emitted for these cases and all existing RUF055 snapshot tests continue to pass: ``` cargo test -p ruff_linter -- "preview_rules::rule_unnecessaryregularexpression" test result: ok. 4 passed; 0 failed; 0 ignored ``` --- .../resources/test/fixtures/ruff/RUF055_0.py | 5 +++++ .../resources/test/fixtures/ruff/RUF055_3.py | 5 ++++- .../ruff/rules/unnecessary_regular_expression.rs | 15 +++++++++++++++ ..._ruff__tests__preview__RUF055_RUF055_0.py.snap | 8 ++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py index 608ea2ef22862..d56da3e484fa7 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py @@ -98,3 +98,8 @@ def dashrepl(matchobj): re.sub(r'abc', "", s) re.sub(r"""abc""", "", s) re.sub(r'''abc''', "", s) + +# Empty pattern: re.split("", s) should not be flagged because +# str.split("") raises ValueError while re.split("", s) succeeds +re.split("", s) +re.split(r"", s) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_3.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_3.py index 13c93f53de85f..b69ab635b51d3 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_3.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_3.py @@ -21,4 +21,7 @@ re.match(rb"ab[c]", b_src) re.search(rb"ab[c]", b_src) re.fullmatch(rb"ab[c]", b_src) -re.split(rb"ab[c]", b_src) \ No newline at end of file +re.split(rb"ab[c]", b_src) + +# Empty pattern: re.split(rb"", b_src) should not be flagged +re.split(rb"", b_src) \ No newline at end of file diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs index 5ccc516ee3de9..2600371cf3b29 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs @@ -118,6 +118,12 @@ pub(crate) fn unnecessary_regular_expression(checker: &Checker, call: &ExprCall) return; } + // `str.split("")` raises `ValueError: empty separator` while `re.split("", s)` succeeds, + // so skip the diagnostic for `re.split` with an empty pattern. + if matches!(re_func.kind, ReFuncKind::Split) && literal.is_empty() { + return; + } + // Now we know the pattern is a string literal with no metacharacters, so // we can proceed with the str method replacement. let new_expr = re_func.replacement(); @@ -362,6 +368,15 @@ enum Literal<'a> { Bytes(&'a ExprBytesLiteral), } +impl Literal<'_> { + fn is_empty(&self) -> bool { + match self { + Literal::Str(str_lit) => str_lit.value.is_empty(), + Literal::Bytes(bytes_lit) => bytes_lit.value.is_empty(), + } + } +} + /// Try to resolve `name` to either a string or bytes literal in `semantic`. fn resolve_literal<'a>(name: &'a Expr, semantic: &'a SemanticModel) -> Option> { if let Some(str_lit) = resolve_string_literal(name, semantic) { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap index c9a75f25c296b..5a20f5c18f779 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap @@ -243,6 +243,7 @@ help: Replace with `s.replace(r'abc', "")` 98 + s.replace(r'abc', "") 99 | re.sub(r"""abc""", "", s) 100 | re.sub(r'''abc''', "", s) +101 | RUF055 [*] Plain string pattern passed to `re` function --> RUF055_0.py:99:1 @@ -260,6 +261,8 @@ help: Replace with `s.replace(r"""abc""", "")` - re.sub(r"""abc""", "", s) 99 + s.replace(r"""abc""", "") 100 | re.sub(r'''abc''', "", s) +101 | +102 | # Empty pattern: re.split("", s) should not be flagged because RUF055 [*] Plain string pattern passed to `re` function --> RUF055_0.py:100:1 @@ -268,6 +271,8 @@ RUF055 [*] Plain string pattern passed to `re` function 99 | re.sub(r"""abc""", "", s) 100 | re.sub(r'''abc''', "", s) | ^^^^^^^^^^^^^^^^^^^^^^^^^ +101 | +102 | # Empty pattern: re.split("", s) should not be flagged because | help: Replace with `s.replace(r'''abc''', "")` 97 | # these double as tests for preserving raw string quoting style @@ -275,3 +280,6 @@ help: Replace with `s.replace(r'''abc''', "")` 99 | re.sub(r"""abc""", "", s) - re.sub(r'''abc''', "", s) 100 + s.replace(r'''abc''', "") +101 | +102 | # Empty pattern: re.split("", s) should not be flagged because +103 | # str.split("") raises ValueError while re.split("", s) succeeds From cd8e8d535fc3e47ac323c2ba0a9c4e8b00c79abe Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Mar 2026 17:31:36 +0000 Subject: [PATCH 171/261] [ty] Add `invalid-enum-member-annotation` lint rule (#23648) ## Summary This PR fixes the last remaining conformance failure on `enums_members.py` in the conformance suite. Implements a new lint rule `invalid-enum-member-annotation` that detects type annotations on enum members. According to the typing spec, enum members should not have explicit type annotations, as the actual runtime type is the enum class itself, not the annotated type. The rule: - Flags annotated enum members (e.g., `DOG: int = 2` in an `Enum` class) - Allows bare `Final` annotations (which don't specify a type) - Excludes dunder names, private names, and special sunder names like `_value_` and `_ignore_` - Excludes pure declarations without values (non-members) ## Test Plan mdtests --- crates/ty/docs/rules.md | 251 +++++++++++------- .../resources/mdtest/enums.md | 109 +++++++- .../src/types/diagnostic.rs | 43 +++ crates/ty_python_semantic/src/types/enums.rs | 52 ++-- .../src/types/infer/builder.rs | 63 ++++- scripts/conformance.py | 1 + ty.schema.json | 10 + 7 files changed, 392 insertions(+), 137 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 00eb3841c08ac..a4db6c70ddf15 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -640,7 +640,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -672,7 +672,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -798,7 +798,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -828,7 +828,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -854,7 +854,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -888,7 +888,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -977,7 +977,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1004,7 +1004,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1032,7 +1032,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1066,7 +1066,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1102,7 +1102,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1126,7 +1126,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1153,7 +1153,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1190,7 +1190,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1222,7 +1222,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1245,13 +1245,62 @@ a: str [assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +## `invalid-enum-member-annotation` + + +Default level: warn · +Added in 0.0.20 · +Related issues · +View source + + + +**What it does** + +Checks for enum members that have explicit type annotations. + +**Why is this bad?** + +The [typing spec] states that type checkers should infer a literal type +for all enum members. An explicit type annotation on an enum member is +misleading because the annotated type will be incorrect — the actual +runtime type is the enum class itself, not the annotated type. + +In CPython's `enum` module, annotated assignments with values are still +treated as members at runtime, but the annotation will confuse readers of the code. + +**Examples** + +```python +from enum import Enum + +class Pet(Enum): + CAT = 1 # OK + DOG: int = 2 # Error: enum members should not be annotated +``` + +Use instead: +```python +from enum import Enum + +class Pet(Enum): + CAT = 1 + DOG = 2 +``` + +**References** + +- [Typing spec: Enum members](https://typing.python.org/en/latest/spec/enums.html#enum-members) + +[typing spec]: https://typing.python.org/en/latest/spec/enums.html#enum-members + ## `invalid-exception-caught` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1295,7 +1344,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1337,7 +1386,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1381,7 +1430,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1419,7 +1468,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1498,7 +1547,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1537,7 +1586,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1598,7 +1647,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1633,7 +1682,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1661,7 +1710,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1695,7 +1744,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1802,7 +1851,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1856,7 +1905,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1886,7 +1935,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1936,7 +1985,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1962,7 +2011,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1993,7 +2042,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2027,7 +2076,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2076,7 +2125,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2105,7 +2154,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2201,7 +2250,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2247,7 +2296,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2274,7 +2323,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2321,7 +2370,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2351,7 +2400,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2381,7 +2430,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2415,7 +2464,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2449,7 +2498,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2480,7 +2529,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2527,7 +2576,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2559,7 +2608,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2594,7 +2643,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2625,7 +2674,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2680,7 +2729,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2723,7 +2772,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2748,7 +2797,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2781,7 +2830,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2810,7 +2859,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2836,7 +2885,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2860,7 +2909,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2893,7 +2942,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2926,7 +2975,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2953,7 +3002,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2980,7 +3029,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3008,7 +3057,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3040,7 +3089,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3077,7 +3126,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3141,7 +3190,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3168,7 +3217,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3200,7 +3249,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3234,7 +3283,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3264,7 +3313,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3293,7 +3342,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3327,7 +3376,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3354,7 +3403,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3382,7 +3431,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3428,7 +3477,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3465,7 +3514,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3489,7 +3538,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3516,7 +3565,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3544,7 +3593,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3602,7 +3651,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3627,7 +3676,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3652,7 +3701,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3691,7 +3740,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3728,7 +3777,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3769,7 +3818,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3870,7 +3919,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3933,7 +3982,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 90f59bad8e520..5455c3dff4e60 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -78,8 +78,7 @@ class Answer(Enum): non_member_1: int - # TODO: this could be considered an error: - non_member_1: str = "some value" + non_member_1: str = "some value" # error: [invalid-enum-member-annotation] # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) @@ -100,6 +99,109 @@ class Answer(Enum): reveal_type(enum_members(Answer)) ``` +### Annotated enum members + +The [typing spec] states that enum members should not have explicit type annotations. Type checkers +should report an error for annotated enum members because the annotation is misleading — the actual +type of an enum member is the enum class itself, not the annotated type. + +```toml +[environment] +python-version = "3.11" +``` + +```py +from enum import Enum, IntEnum, StrEnum, member +from typing import Callable, Final + +class Pet(Enum): + CAT = 1 + DOG: int = 2 # error: [invalid-enum-member-annotation] "Type annotation on enum member `DOG` is not allowed" + BIRD: str = "bird" # error: [invalid-enum-member-annotation] +``` + +Bare `Final` annotations are allowed (they don't specify a type): + +```py +class Pet2(Enum): + CAT: Final = 1 # OK + DOG: Final = 2 # OK +``` + +But `Final` with a type argument is not allowed: + +```py +class Pet3(Enum): + CAT: Final[int] = 1 # error: [invalid-enum-member-annotation] + DOG: Final[str] = "woof" # error: [invalid-enum-member-annotation] +``` + +`enum.member` used as value wrapper is the standard way to declare members explicitly: + +```py +class Pet4(Enum): + CAT = member(1) # OK +``` + +Dunder and private names are not enum members, so they don't trigger the diagnostic: + +```py +class Pet5(Enum): + CAT = 1 + __private: int = 2 # OK: dunder/private names are never members + __module__: str = "my_module" # OK +``` + +Pure declarations (annotations without values) are non-members and are fine: + +```py +class Pet6(Enum): + CAT = 1 + species: str # OK: no value, so this is a non-member declaration +``` + +Callable values are never enum members at runtime, so annotating them is fine: + +```py +def identity(x: int) -> int: + return x + +class Pet7(Enum): + CAT = 1 + declared_callable: Callable[[int], int] = identity # OK: callables are never members +``` + +The check also works for subclasses of `Enum`: + +```py +class Status(IntEnum): + OK: int = 200 # error: [invalid-enum-member-annotation] + NOT_FOUND = 404 # OK + +class Color(StrEnum): + RED: str = "red" # error: [invalid-enum-member-annotation] + GREEN = "green" # OK +``` + +Special sunder names like `_value_` and `_ignore_` are not flagged: + +```py +class Pet8(Enum): + _value_: int = 0 # OK: `_value_` is a special enum name + _ignore_: str = "TEMP" # OK: `_ignore_` is a special enum name + CAT = 1 +``` + +Names listed in `_ignore_` are not members, so annotating them is fine: + +```py +class Pet9(Enum): + _ignore_ = "A B" + A: int = 42 # OK: `A` is listed in `_ignore_` + B: str = "hello" # OK: `B` is listed in `_ignore_` + C: int = 3 # error: [invalid-enum-member-annotation] +``` + ### Declared `_value_` annotation If a `_value_` annotation is defined on an `Enum` class, all enum member values must be compatible @@ -814,7 +916,7 @@ class Answer(Enum): def is_yes(self) -> bool: return self == Answer.YES - constant: int = 1 + constant: int = 1 # error: [invalid-enum-member-annotation] reveal_type(Answer.YES.is_yes()) # revealed: bool reveal_type(Answer.YES.constant) # revealed: int @@ -1353,3 +1455,4 @@ class MyEnum[T](MyEnumBase): - Documentation: [class-private names]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers +[typing spec]: https://typing.python.org/en/latest/spec/enums.html#enum-members diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index ae3ee181c4cc8..d8910b48dcdb5 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -83,6 +83,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_CONTEXT_MANAGER); registry.register_lint(&INVALID_DECLARATION); registry.register_lint(&INVALID_EXCEPTION_CAUGHT); + registry.register_lint(&INVALID_ENUM_MEMBER_ANNOTATION); registry.register_lint(&INVALID_GENERIC_ENUM); registry.register_lint(&INVALID_GENERIC_CLASS); registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE); @@ -1193,6 +1194,48 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for enum members that have explicit type annotations. + /// + /// ## Why is this bad? + /// The [typing spec] states that type checkers should infer a literal type + /// for all enum members. An explicit type annotation on an enum member is + /// misleading because the annotated type will be incorrect — the actual + /// runtime type is the enum class itself, not the annotated type. + /// + /// In CPython's `enum` module, annotated assignments with values are still + /// treated as members at runtime, but the annotation will confuse readers of the code. + /// + /// ## Examples + /// ```python + /// from enum import Enum + /// + /// class Pet(Enum): + /// CAT = 1 # OK + /// DOG: int = 2 # Error: enum members should not be annotated + /// ``` + /// + /// Use instead: + /// ```python + /// from enum import Enum + /// + /// class Pet(Enum): + /// CAT = 1 + /// DOG = 2 + /// ``` + /// + /// ## References + /// - [Typing spec: Enum members](https://typing.python.org/en/latest/spec/enums.html#enum-members) + /// + /// [typing spec]: https://typing.python.org/en/latest/spec/enums.html#enum-members + pub(crate) static INVALID_ENUM_MEMBER_ANNOTATION = { + summary: "detects type annotations on enum members", + status: LintStatus::stable("0.0.20"), + default_level: Level::Warn, + } +} + declare_lint! { /// ## What it does /// Checks for enum classes that are also generic. diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 19d920662914b..0f724641a7b5b 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -71,6 +71,36 @@ impl<'db> EnumMetadata<'db> { } } +/// Returns the set of names listed in an enum's `_ignore_` attribute. +#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] +pub(crate) fn enum_ignored_names<'db>(db: &'db dyn Db, scope_id: ScopeId<'db>) -> FxHashSet { + let use_def_map = use_def_map(db, scope_id); + let table = place_table(db, scope_id); + + let Some(ignore) = table.symbol_id("_ignore_") else { + return FxHashSet::default(); + }; + + let ignore_bindings = use_def_map.reachable_symbol_bindings(ignore); + let ignore_place = place_from_bindings(db, ignore_bindings).place; + + match ignore_place { + Place::Defined(DefinedPlace { ty, .. }) => ty + .as_string_literal() + .map(|ignored_names| { + ignored_names + .value(db) + .split_ascii_whitespace() + .map(Name::new) + .collect() + }) + .unwrap_or_default(), + + // TODO: support the list-variant of `_ignore_`. + Place::Undefined => FxHashSet::default(), + } +} + /// List all members of an enum. #[allow(clippy::ref_option, clippy::unnecessary_wraps)] #[salsa::tracked(returns(as_ref), cycle_initial=|_, _, _| Some(EnumMetadata::empty()), heap_size=ruff_memory_usage::heap_size)] @@ -114,21 +144,7 @@ pub(crate) fn enum_metadata<'db>( let mut enum_values: FxHashMap, Name> = FxHashMap::default(); let mut auto_counter = 0; let mut auto_members = FxHashSet::default(); - let ignored_names: Option> = if let Some(ignore) = table.symbol_id("_ignore_") { - let ignore_bindings = use_def_map.reachable_symbol_bindings(ignore); - let ignore_place = place_from_bindings(db, ignore_bindings).place; - - match ignore_place { - Place::Defined(DefinedPlace { ty, .. }) => ty - .as_string_literal() - .map(|ignored_names| ignored_names.value(db).split_ascii_whitespace().collect()), - - // TODO: support the list-variant of `_ignore_`. - Place::Undefined => None, - } - } else { - None - }; + let ignored_names = enum_ignored_names(db, scope_id); let mut aliases = FxHashMap::default(); @@ -143,11 +159,7 @@ pub(crate) fn enum_metadata<'db>( return None; } - if name == "_ignore_" - || ignored_names - .as_ref() - .is_some_and(|names| names.contains(&name.as_str())) - { + if name == "_ignore_" || ignored_names.contains(name) { // Skip ignored attributes return None; } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 879992eb50006..243e3e8a1c9e2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -74,18 +74,18 @@ use crate::types::diagnostic::{ DATACLASS_FIELD_ORDER, DUPLICATE_BASE, DUPLICATE_KW_ONLY, FINAL_ON_NON_METHOD, FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DATACLASS, - INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, INVALID_KEY, - INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, - INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, - INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, - INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, - INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, MISSING_ARGUMENT, - NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE, - POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, - TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, - UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, - UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, + INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_GENERIC_CLASS, + INVALID_GENERIC_ENUM, INVALID_KEY, INVALID_LEGACY_POSITIONAL_PARAMETER, + INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, + INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, + INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, + INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, + INVALID_TYPE_VARIABLE_DEFAULT, INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, + IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, + POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, + SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, + UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, + UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, @@ -107,7 +107,7 @@ use crate::types::diagnostic::{ report_shadowed_type_variable, report_unsupported_augmented_assignment, report_unsupported_base, report_unsupported_comparison, }; -use crate::types::enums::is_enum_class_by_inheritance; +use crate::types::enums::{enum_ignored_names, is_enum_class_by_inheritance}; use crate::types::function::{ FunctionBodyKind, FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, function_body_kind, is_implicit_classmethod, @@ -9635,6 +9635,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &DeclaredAndInferredType::AreTheSame(TypeAndQualifiers::declared(inferred_ty)), ); } else { + // Check for annotated enum members. The typing spec states that enum + // members should not have explicit type annotations. + if let Some(name_expr) = target.as_name_expr() + && !name_expr.id.starts_with("__") + && !matches!(name_expr.id.as_str(), "_ignore_" | "_value_" | "_name_") + // Not bare Final (bare Final is allowed on enum members) + && !(declared.qualifiers.contains(TypeQualifiers::FINAL) + && matches!(declared.inner_type(), Type::Dynamic(DynamicType::Unknown))) + // Value type would be an enum member at runtime (exclude callables, + // which are never members) + && !inferred_ty.is_subtype_of( + self.db(), + Type::Callable(CallableType::unknown(self.db())) + .top_materialization(self.db()), + ) + { + let current_scope_id = self.scope().file_scope_id(self.db()); + let current_scope = self.index.scope(current_scope_id); + if current_scope.kind() == ScopeKind::Class + && let Some(class) = + nearest_enclosing_class(self.db(), self.index, self.scope()) + && is_enum_class_by_inheritance(self.db(), class) + && !enum_ignored_names(self.db(), self.scope()).contains(&name_expr.id) + && let Some(builder) = self + .context + .report_lint(&INVALID_ENUM_MEMBER_ANNOTATION, annotation) + { + let mut diag = builder.into_diagnostic(format_args!( + "Type annotation on enum member `{}` is not allowed", + &name_expr.id + )); + diag.info( + "See: https://typing.python.org/en/latest/spec/enums.html#enum-members", + ); + } + } + self.add_declaration_with_binding( target.into(), definition, diff --git a/scripts/conformance.py b/scripts/conformance.py index a3680619daeaf..6e492d1c1b307 100644 --- a/scripts/conformance.py +++ b/scripts/conformance.py @@ -470,6 +470,7 @@ def collect_ty_diagnostics( f"--python-version={python_version}", "--output-format=gitlab", "--ignore=assert-type-unspellable-subtype", + "--error=invalid-enum-member-annotation", "--error=invalid-legacy-positional-parameter", "--error=deprecated", "--error=redundant-final-classvar", diff --git a/ty.schema.json b/ty.schema.json index f3204fc366ccc..5e43d4cd15c68 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -735,6 +735,16 @@ } ] }, + "invalid-enum-member-annotation": { + "title": "detects type annotations on enum members", + "description": "## What it does\nChecks for enum members that have explicit type annotations.\n\n## Why is this bad?\nThe [typing spec] states that type checkers should infer a literal type\nfor all enum members. An explicit type annotation on an enum member is\nmisleading because the annotated type will be incorrect — the actual\nruntime type is the enum class itself, not the annotated type.\n\nIn CPython's `enum` module, annotated assignments with values are still\ntreated as members at runtime, but the annotation will confuse readers of the code.\n\n## Examples\n```python\nfrom enum import Enum\n\nclass Pet(Enum):\n CAT = 1 # OK\n DOG: int = 2 # Error: enum members should not be annotated\n```\n\nUse instead:\n```python\nfrom enum import Enum\n\nclass Pet(Enum):\n CAT = 1\n DOG = 2\n```\n\n## References\n- [Typing spec: Enum members](https://typing.python.org/en/latest/spec/enums.html#enum-members)\n\n[typing spec]: https://typing.python.org/en/latest/spec/enums.html#enum-members", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-exception-caught": { "title": "detects exception handlers that catch classes that do not inherit from `BaseException`", "description": "## What it does\nChecks for exception handlers that catch non-exception classes.\n\n## Why is this bad?\nCatching classes that do not inherit from `BaseException` will raise a `TypeError` at runtime.\n\n## Example\n```python\ntry:\n 1 / 0\nexcept 1:\n ...\n```\n\nUse instead:\n```python\ntry:\n 1 / 0\nexcept ZeroDivisionError:\n ...\n```\n\n## References\n- [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)\n\n## Ruff rule\n This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes)", From d98b514a1c592797035bf09a424cc72ee78fa5b6 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Mar 2026 17:32:03 +0000 Subject: [PATCH 172/261] [ty] Move `KnownInstanceType`, and related types, to a new `known_instance.rs` submodule (#23680) --- crates/ty_python_semantic/src/types.rs | 535 +---------------- .../ty_python_semantic/src/types/call/bind.rs | 11 +- crates/ty_python_semantic/src/types/class.rs | 7 +- .../ty_python_semantic/src/types/function.rs | 9 +- .../src/types/known_instance.rs | 561 ++++++++++++++++++ .../ty_python_semantic/src/types/visitor.rs | 7 +- 6 files changed, 586 insertions(+), 544 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/known_instance.rs diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7d33783a9ff6a..e1304f3260569 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -31,6 +31,7 @@ pub(crate) use self::infer::{ TypeContext, infer_complete_scope_types, infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, infer_scope_types, }; +pub use self::known_instance::KnownInstanceType; pub(crate) use self::set_theoretic::builder::{IntersectionBuilder, UnionBuilder}; pub use self::set_theoretic::{ IntersectionType, NegativeIntersectionElements, NegativeIntersectionElementsIterator, UnionType, @@ -51,10 +52,9 @@ use crate::semantic_index::{imported_modules, place_table, semantic_index}; use crate::suppression::check_suppressions; use crate::types::bound_super::BoundSuperType; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; -use crate::types::class::NamedTupleSpec; pub(crate) use crate::types::class_base::ClassBase; use crate::types::constraints::{ - ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, OwnedConstraintSet, + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; @@ -65,9 +65,10 @@ use crate::types::function::{ FunctionType, KnownFunction, }; use crate::types::generics::{ - ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, walk_generic_context, + ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, }; pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; +use crate::types::known_instance::{InternedConstraintSet, InternedType, UnionTypeInstance}; use crate::types::mro::{Mro, MroIterator, StaticMroError}; pub(crate) use crate::types::narrow::{ NarrowingConstraint, PossiblyNarrowedPlaces, PossiblyNarrowedPlacesBuilder, @@ -109,6 +110,7 @@ mod generics; pub mod ide_support; mod infer; mod instance; +mod known_instance; pub mod list_members; mod literal; mod member; @@ -6141,75 +6143,7 @@ impl<'db> Type<'db> { match self { Type::TypeVar(bound_typevar) => bound_typevar.apply_type_mapping_impl(db, type_mapping, visitor), - - Type::KnownInstance(known_instance) => match known_instance { - KnownInstanceType::TypeVar(typevar) => { - match type_mapping { - TypeMapping::BindLegacyTypevars(binding_context) => { - Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context, None)) - } - TypeMapping::ApplySpecialization(_) | - TypeMapping::UniqueSpecialization { .. } | - TypeMapping::PromoteLiterals(_) | - TypeMapping::BindSelf(..) | - TypeMapping::ReplaceSelf { .. } | - TypeMapping::Materialize(_) | - TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion | - TypeMapping::RescopeReturnCallables(_) => self, - } - } - KnownInstanceType::UnionType(instance) => { - if let Ok(union_type) = instance.union_type(db) { - Type::KnownInstance(KnownInstanceType::UnionType( - UnionTypeInstance::new( - db, - instance._value_expr_types(db), - Ok(union_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor) - ) - ))) - } else { - self - } - }, - KnownInstanceType::Annotated(ty) => { - Type::KnownInstance(KnownInstanceType::Annotated( - InternedType::new( - db, - ty.inner(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), - ) - )) - }, - KnownInstanceType::Callable(callable_type) => { - Type::KnownInstance(KnownInstanceType::Callable( - callable_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor), - )) - }, - KnownInstanceType::TypeGenericAlias(ty) => { - Type::KnownInstance(KnownInstanceType::TypeGenericAlias( - InternedType::new( - db, - ty.inner(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), - ) - )) - }, - - KnownInstanceType::SubscriptedProtocol(_) | - KnownInstanceType::SubscriptedGeneric(_) | - KnownInstanceType::TypeAliasType(_) | - KnownInstanceType::Deprecated(_) | - KnownInstanceType::Field(_) | - KnownInstanceType::ConstraintSet(_) | - KnownInstanceType::GenericContext(_) | - KnownInstanceType::Specialization(_) | - KnownInstanceType::Literal(_) | - KnownInstanceType::LiteralStringAlias(_) | - KnownInstanceType::NamedTupleSpec(_) | - KnownInstanceType::NewType(_) => { - // TODO: For some of these, we may need to apply the type mapping to inner types. - self - }, - } + Type::KnownInstance(known_instance) => known_instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), Type::FunctionLiteral(function) => visitor.visit(self, || { match type_mapping { @@ -7276,260 +7210,6 @@ impl<'db> TypeMapping<'_, 'db> { } } -/// A Salsa-interned constraint set. This is only needed to have something appropriately small to -/// put in a [`KnownInstance::ConstraintSet`]. We don't actually manipulate these as part of using -/// constraint sets to check things like assignability; they're only used as a debugging aid in -/// mdtests. In theory, that means there's no need for this to be interned; being tracked would be -/// sufficient. However, we currently think that tracked structs are unsound w.r.t. salsa cycles, -/// so out of an abundance of caution, we are interning the struct. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct InternedConstraintSet<'db> { - #[returns(ref)] - constraints: OwnedConstraintSet<'db>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for InternedConstraintSet<'_> {} - -/// Singleton types that are heavily special-cased by ty. Despite its name, -/// quite a different type to [`NominalInstanceType`]. -/// -/// In many ways, this enum behaves similarly to [`SpecialFormType`]. -/// Unlike instances of that variant, however, `Type::KnownInstance`s do not exist -/// at a location that can be known prior to any analysis by ty, and each variant -/// of `KnownInstanceType` can have multiple instances (as, unlike `SpecialFormType`, -/// `KnownInstanceType` variants can hold associated data). Instances of this type -/// are generally created by operations at runtime in some way, such as a type alias -/// statement, a typevar definition, or an instance of `Generic[T]` in a class's -/// bases list. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] -pub enum KnownInstanceType<'db> { - /// The type of `Protocol[T]`, `Protocol[U, S]`, etc -- usually only found in a class's bases list. - /// - /// Note that unsubscripted `Protocol` is represented by [`SpecialFormType::Protocol`], not this type. - SubscriptedProtocol(GenericContext<'db>), - - /// The type of `Generic[T]`, `Generic[U, S]`, etc -- usually only found in a class's bases list. - /// - /// Note that unsubscripted `Generic` is represented by [`SpecialFormType::Generic`], not this type. - SubscriptedGeneric(GenericContext<'db>), - - /// A single instance of `typing.TypeVar` - TypeVar(TypeVarInstance<'db>), - - /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) - TypeAliasType(TypeAliasType<'db>), - - /// A single instance of `warnings.deprecated` or `typing_extensions.deprecated` - Deprecated(DeprecatedInstance<'db>), - - /// A single instance of `dataclasses.Field` - Field(FieldInstance<'db>), - - /// A constraint set, which is exposed in mdtests as an instance of - /// `ty_extensions.ConstraintSet`. - ConstraintSet(InternedConstraintSet<'db>), - - /// A generic context, which is exposed in mdtests as an instance of - /// `ty_extensions.GenericContext`. - GenericContext(GenericContext<'db>), - - /// A specialization, which is exposed in mdtests as an instance of - /// `ty_extensions.Specialization`. - Specialization(Specialization<'db>), - - /// A single instance of `types.UnionType`, which stores the elements of - /// a PEP 604 union, or a `typing.Union`. - UnionType(UnionTypeInstance<'db>), - - /// A single instance of `typing.Literal` - Literal(InternedType<'db>), - - /// A single instance of `typing.Annotated` - Annotated(InternedType<'db>), - - /// An instance of `typing.GenericAlias` representing a `type[...]` expression. - TypeGenericAlias(InternedType<'db>), - - /// An instance of `typing.GenericAlias` representing a `Callable[...]` expression. - Callable(CallableType<'db>), - - /// A literal string which is the right-hand side of a PEP 613 `TypeAlias`. - LiteralStringAlias(InternedType<'db>), - - /// An identity callable created with `typing.NewType(name, base)`, which behaves like a - /// subtype of `base` in type expressions. See the `struct NewType` payload for an example. - NewType(NewType<'db>), - - /// The inferred spec for a functional `NamedTuple` class. - NamedTupleSpec(NamedTupleSpec<'db>), -} - -fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - known_instance: KnownInstanceType<'db>, - visitor: &V, -) { - match known_instance { - KnownInstanceType::SubscriptedProtocol(context) - | KnownInstanceType::SubscriptedGeneric(context) => { - walk_generic_context(db, context, visitor); - } - KnownInstanceType::TypeVar(typevar) => { - visitor.visit_type_var_type(db, typevar); - } - KnownInstanceType::TypeAliasType(type_alias) => { - visitor.visit_type_alias_type(db, type_alias); - } - KnownInstanceType::Deprecated(_) - | KnownInstanceType::ConstraintSet(_) - | KnownInstanceType::GenericContext(_) - | KnownInstanceType::Specialization(_) => { - // Nothing to visit - } - KnownInstanceType::Field(field) => { - if let Some(default_ty) = field.default_type(db) { - visitor.visit_type(db, default_ty); - } - } - KnownInstanceType::UnionType(instance) => { - if let Ok(union_type) = instance.union_type(db) { - visitor.visit_type(db, *union_type); - } - } - KnownInstanceType::Literal(ty) - | KnownInstanceType::Annotated(ty) - | KnownInstanceType::TypeGenericAlias(ty) - | KnownInstanceType::LiteralStringAlias(ty) => { - visitor.visit_type(db, ty.inner(db)); - } - KnownInstanceType::Callable(callable) => { - visitor.visit_callable_type(db, callable); - } - KnownInstanceType::NewType(newtype) => { - visitor.visit_type(db, newtype.concrete_base_type(db)); - } - KnownInstanceType::NamedTupleSpec(spec) => { - for field in spec.fields(db) { - visitor.visit_type(db, field.ty); - } - } - } -} - -impl<'db> VarianceInferable<'db> for KnownInstanceType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { - match self { - KnownInstanceType::TypeAliasType(type_alias) => { - type_alias.raw_value_type(db).variance_of(db, typevar) - } - _ => TypeVarVariance::Bivariant, - } - } -} - -impl<'db> KnownInstanceType<'db> { - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - match self { - // Nothing to normalize - Self::SubscriptedProtocol(context) => Some(Self::SubscriptedProtocol(context)), - Self::SubscriptedGeneric(context) => Some(Self::SubscriptedGeneric(context)), - Self::Deprecated(deprecated) => Some(Self::Deprecated(deprecated)), - Self::ConstraintSet(set) => Some(Self::ConstraintSet(set)), - Self::TypeVar(typevar) => Some(Self::TypeVar(typevar)), - Self::TypeAliasType(type_alias) => Some(Self::TypeAliasType(type_alias)), - Self::Field(field) => field - .recursive_type_normalized_impl(db, div, nested) - .map(Self::Field), - Self::UnionType(union_type) => union_type - .recursive_type_normalized_impl(db, div, nested) - .map(Self::UnionType), - Self::Literal(ty) => ty - .recursive_type_normalized_impl(db, div, true) - .map(Self::Literal), - Self::Annotated(ty) => ty - .recursive_type_normalized_impl(db, div, true) - .map(Self::Annotated), - Self::TypeGenericAlias(ty) => ty - .recursive_type_normalized_impl(db, div, true) - .map(Self::TypeGenericAlias), - Self::LiteralStringAlias(ty) => ty - .recursive_type_normalized_impl(db, div, true) - .map(Self::LiteralStringAlias), - Self::Callable(callable) => callable - .recursive_type_normalized_impl(db, div, nested) - .map(Self::Callable), - Self::NewType(newtype) => newtype - .try_map_base_class_type(db, |class_type| { - class_type.recursive_type_normalized_impl(db, div, true) - }) - .map(Self::NewType), - Self::GenericContext(generic) => Some(Self::GenericContext(generic)), - Self::Specialization(specialization) => specialization - .recursive_type_normalized_impl(db, div, true) - .map(Self::Specialization), - Self::NamedTupleSpec(spec) => spec - .recursive_type_normalized_impl(db, div, true) - .map(Self::NamedTupleSpec), - } - } - - fn class(self, db: &'db dyn Db) -> KnownClass { - match self { - Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm, - Self::TypeVar(typevar_instance) if typevar_instance.is_paramspec(db) => { - KnownClass::ParamSpec - } - Self::TypeVar(_) => KnownClass::TypeVar, - Self::TypeAliasType(TypeAliasType::PEP695(alias)) if alias.is_specialized(db) => { - KnownClass::GenericAlias - } - Self::TypeAliasType(_) => KnownClass::TypeAliasType, - Self::Deprecated(_) => KnownClass::Deprecated, - Self::Field(_) => KnownClass::Field, - Self::ConstraintSet(_) => KnownClass::ConstraintSet, - Self::GenericContext(_) => KnownClass::GenericContext, - Self::Specialization(_) => KnownClass::Specialization, - Self::UnionType(_) => KnownClass::UnionType, - Self::Literal(_) - | Self::Annotated(_) - | Self::TypeGenericAlias(_) - | Self::Callable(_) => KnownClass::GenericAlias, - Self::LiteralStringAlias(_) => KnownClass::Str, - Self::NewType(_) => KnownClass::NewType, - Self::NamedTupleSpec(_) => KnownClass::Sequence, - } - } - - fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - self.class(db).to_class_literal(db) - } - - /// Return the instance type which this type is a subtype of. - /// - /// For example, an alias created using the `type` statement is an instance of - /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db)` - /// returns `Type::NominalInstance(NominalInstanceType { class: })`. - fn instance_fallback(self, db: &dyn Db) -> Type<'_> { - self.class(db).to_instance(db) - } - - /// Return `true` if this symbol is an instance of `class`. - fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { - self.class(db).is_subclass_of(db, class) - } - - /// Return the repr of the symbol at runtime - fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { - self.display_with(db, DisplaySettings::default()) - } -} - /// A type that is determined to be divergent during recursive type inference. /// This type must never be eliminated by dynamic type reduction /// (e.g. `Divergent` is assignable to `@Todo`, but `@Todo | Divergent` must not be reducted to `@Todo`). @@ -7960,63 +7640,6 @@ impl<'db> InvalidTypeExpression<'db> { } } -/// Data regarding a `warnings.deprecated` or `typing_extensions.deprecated` decorator. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct DeprecatedInstance<'db> { - /// The message for the deprecation - pub message: Option>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for DeprecatedInstance<'_> {} - -/// Contains information about instances of `dataclasses.Field`, typically created using -/// `dataclasses.field()`. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct FieldInstance<'db> { - /// The type of the default value for this field. This is derived from the `default` or - /// `default_factory` arguments to `dataclasses.field()`. - pub default_type: Option>, - - /// Whether this field is part of the `__init__` signature, or not. - pub init: bool, - - /// Whether or not this field can only be passed as a keyword argument to `__init__`. - pub kw_only: Option, - - /// This name is used to provide an alternative parameter name in the synthesized `__init__` method. - pub alias: Option>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for FieldInstance<'_> {} - -impl<'db> FieldInstance<'db> { - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - let default_type = match self.default_type(db) { - Some(default) if nested => Some(default.recursive_type_normalized_impl(db, div, true)?), - Some(default) => Some( - default - .recursive_type_normalized_impl(db, div, true) - .unwrap_or(div), - ), - None => None, - }; - Some(FieldInstance::new( - db, - default_type, - self.init(db), - self.kw_only(db), - self.alias(db), - )) - } -} - /// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax, /// or an implicit typevar like `Self` was used. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] @@ -9207,152 +8830,6 @@ impl InferredAs { } } -/// Contains information about a `types.UnionType` instance built from a PEP 604 -/// union or a legacy `typing.Union[…]` annotation in a value expression context, -/// e.g. `IntOrStr = int | str` or `IntOrStr = Union[int, str]`. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct UnionTypeInstance<'db> { - /// The types of the elements of this union, as they were inferred in a value - /// expression context. For `int | str`, this would contain `` and - /// ``. For `Union[int, str]`, this field is `None`, as we infer - /// the elements as type expressions. Use `value_expression_types` to get the - /// corresponding value expression types. - #[returns(ref)] - _value_expr_types: Option<[Type<'db>; 2]>, - - /// The type of the full union, which can be used when this `UnionType` instance - /// is used in a type expression context. For `int | str`, this would contain - /// `Ok(int | str)`. If any of the element types could not be converted, this - /// contains the first encountered error. - #[returns(ref)] - union_type: Result, InvalidTypeExpressionError<'db>>, -} - -impl get_size2::GetSize for UnionTypeInstance<'_> {} - -impl<'db> UnionTypeInstance<'db> { - pub(crate) fn from_value_expression_types( - db: &'db dyn Db, - value_expr_types: [Type<'db>; 2], - scope_id: ScopeId<'db>, - typevar_binding_context: Option>, - ) -> Type<'db> { - let mut builder = UnionBuilder::new(db); - for ty in &value_expr_types { - match ty.in_type_expression(db, scope_id, typevar_binding_context) { - Ok(ty) => builder.add_in_place(ty), - Err(error) => { - return Type::KnownInstance(KnownInstanceType::UnionType( - UnionTypeInstance::new(db, Some(value_expr_types), Err(error)), - )); - } - } - } - - Type::KnownInstance(KnownInstanceType::UnionType(UnionTypeInstance::new( - db, - Some(value_expr_types), - Ok(builder.build()), - ))) - } - - /// Get the types of the elements of this union as they would appear in a value - /// expression context. For a PEP 604 union, we return the actual types that were - /// inferred when we encountered the union in a value expression context. For a - /// legacy `typing.Union[…]` annotation, we turn the type-expression types into - /// their corresponding value-expression types, i.e. we turn instances like `int` - /// into class literals like ``. This operation is potentially lossy. - pub(crate) fn value_expression_types( - self, - db: &'db dyn Db, - ) -> Result> + 'db, InvalidTypeExpressionError<'db>> { - let to_class_literal = |ty: Type<'db>| { - ty.as_nominal_instance() - .and_then(|instance| { - instance - .class(db) - .static_class_literal(db) - .map(|(lit, _)| Type::ClassLiteral(lit.into())) - }) - .unwrap_or_else(Type::unknown) - }; - - if let Some(value_expr_types) = self._value_expr_types(db) { - Ok(Either::Left(value_expr_types.iter().copied())) - } else { - match self.union_type(db).clone()? { - Type::Union(union) => Ok(Either::Right(Either::Left( - union.elements(db).iter().copied().map(to_class_literal), - ))), - ty => Ok(Either::Right(Either::Right(std::iter::once( - to_class_literal(ty), - )))), - } - } - } - - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - // The `Divergent` elimination rules are different within union types. - // See `UnionType::recursive_type_normalized_impl` for details. - let value_expr_types = match self._value_expr_types(db).as_ref() { - Some([first, second]) if nested => Some([ - first.recursive_type_normalized_impl(db, div, nested)?, - second.recursive_type_normalized_impl(db, div, nested)?, - ]), - Some([first, second]) => Some([ - first - .recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div), - second - .recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div), - ]), - None => None, - }; - let union_type = match self.union_type(db).clone() { - Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, div, nested)?), - Ok(ty) => Ok(ty - .recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div)), - Err(err) => Err(err), - }; - - Some(Self::new(db, value_expr_types, union_type)) - } -} - -/// A salsa-interned `Type` -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct InternedType<'db> { - inner: Type<'db>, -} - -impl get_size2::GetSize for InternedType<'_> {} - -impl<'db> InternedType<'db> { - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - let inner = if nested { - self.inner(db) - .recursive_type_normalized_impl(db, div, nested)? - } else { - self.inner(db) - .recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div) - }; - Some(InternedType::new(db, inner)) - } -} - /// Error returned if a type is not awaitable. #[derive(Debug)] enum AwaitError<'db> { diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index f76ab6ca01672..152847048c90d 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -40,15 +40,16 @@ use crate::types::function::{ use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError, }; +use crate::types::known_instance::FieldInstance; use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::{ BoundMethodType, BoundTypeVarIdentity, BoundTypeVarInstance, CallableSignature, CallableType, - CallableTypeKind, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, - FieldInstance, GenericAlias, InternedConstraintSet, IntersectionType, KnownBoundMethodType, - KnownClass, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, NominalInstanceType, - PropertyInstanceType, SpecialFormType, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, - TypeVarVariance, UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, + CallableTypeKind, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, GenericAlias, + InternedConstraintSet, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, + LiteralValueTypeKind, MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, + SpecialFormType, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, TypeVarVariance, + UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, }; use crate::unpack::EvaluationMode; use crate::{DisplaySettings, Program}; diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 9df5d4842c9b7..7d301c8ecfc00 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -33,6 +33,7 @@ use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, walk_specialization, }; use crate::types::infer::{infer_expression_type, infer_unpack_types, nearest_enclosing_class}; +use crate::types::known_instance::DeprecatedInstance; use crate::types::member::{Member, class_member}; use crate::types::mro::DynamicMroError; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; @@ -43,9 +44,9 @@ use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion use crate::types::{ ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType, CallableTypeKind, CallableTypes, DATACLASS_FLAGS, DataclassFlags, DataclassParams, - DeprecatedInstance, FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownInstanceType, - MaterializationKind, PropertyInstanceType, TypeContext, TypeMapping, TypedDictParams, - UnionBuilder, VarianceInferable, binding_type, declaration_type, determine_upper_bound, + FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownInstanceType, MaterializationKind, + PropertyInstanceType, TypeContext, TypeMapping, TypedDictParams, UnionBuilder, + VarianceInferable, binding_type, declaration_type, determine_upper_bound, }; use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index d6b1f4ef8b8fb..d7246e915430e 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -78,6 +78,7 @@ use crate::types::diagnostic::{ use crate::types::display::DisplaySettings; use crate::types::generics::{GenericContext, InferableTypeVars, typing_self}; use crate::types::infer::nearest_enclosing_class; +use crate::types::known_instance::DeprecatedInstance; use crate::types::list_members::all_members; use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; @@ -85,10 +86,10 @@ use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; use crate::types::{ ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, CallableTypeKind, - ClassBase, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, - KnownClass, KnownInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, - Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, UnionBuilder, UnionType, - binding_type, definition_expression_type, infer_definition_types, walk_signature, + ClassBase, ClassLiteral, ClassType, DynamicType, FindLegacyTypeVarsVisitor, KnownClass, + KnownInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, + TypeContext, TypeMapping, TypeVarBoundOrConstraints, UnionBuilder, UnionType, binding_type, + definition_expression_type, infer_definition_types, walk_signature, }; use crate::{Db, FxOrderSet}; diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs new file mode 100644 index 0000000000000..33d2fd55eada4 --- /dev/null +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -0,0 +1,561 @@ +use itertools::Either; + +use crate::{ + Db, DisplaySettings, + semantic_index::{definition::Definition, scope::ScopeId}, + types::{ + ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassType, GenericContext, + InvalidTypeExpressionError, KnownClass, StringLiteralType, Type, TypeAliasType, + TypeContext, TypeMapping, TypeVarInstance, TypeVarVariance, UnionBuilder, + class::NamedTupleSpec, + constraints::OwnedConstraintSet, + generics::{Specialization, walk_generic_context}, + newtype::NewType, + variance::VarianceInferable, + visitor, + }, +}; + +/// A Salsa-interned constraint set. This is only needed to have something appropriately small to +/// put in a [`KnownInstance::ConstraintSet`]. We don't actually manipulate these as part of using +/// constraint sets to check things like assignability; they're only used as a debugging aid in +/// mdtests. In theory, that means there's no need for this to be interned; being tracked would be +/// sufficient. However, we currently think that tracked structs are unsound w.r.t. salsa cycles, +/// so out of an abundance of caution, we are interning the struct. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct InternedConstraintSet<'db> { + #[returns(ref)] + pub(super) constraints: OwnedConstraintSet<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for InternedConstraintSet<'_> {} + +/// Singleton types that are heavily special-cased by ty. Despite its name, +/// quite a different type to [`super::NominalInstanceType`]. +/// +/// In many ways, this enum behaves similarly to [`super::SpecialFormType`]. +/// Unlike instances of that variant, however, `Type::KnownInstance`s do not exist +/// at a location that can be known prior to any analysis by ty, and each variant +/// of `KnownInstanceType` can have multiple instances (as, unlike `SpecialFormType`, +/// `KnownInstanceType` variants can hold associated data). Instances of this type +/// are generally created by operations at runtime in some way, such as a type alias +/// statement, a typevar definition, or an instance of `Generic[T]` in a class's +/// bases list. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] +pub enum KnownInstanceType<'db> { + /// The type of `Protocol[T]`, `Protocol[U, S]`, etc -- usually only found in a class's bases list. + /// + /// Note that unsubscripted `Protocol` is represented by [`super::SpecialFormType::Protocol`], not this type. + SubscriptedProtocol(GenericContext<'db>), + + /// The type of `Generic[T]`, `Generic[U, S]`, etc -- usually only found in a class's bases list. + /// + /// Note that unsubscripted `Generic` is represented by [`super::SpecialFormType::Generic`], not this type. + SubscriptedGeneric(GenericContext<'db>), + + /// A single instance of `typing.TypeVar` + TypeVar(TypeVarInstance<'db>), + + /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) + TypeAliasType(TypeAliasType<'db>), + + /// A single instance of `warnings.deprecated` or `typing_extensions.deprecated` + Deprecated(DeprecatedInstance<'db>), + + /// A single instance of `dataclasses.Field` + Field(FieldInstance<'db>), + + /// A constraint set, which is exposed in mdtests as an instance of + /// `ty_extensions.ConstraintSet`. + ConstraintSet(InternedConstraintSet<'db>), + + /// A generic context, which is exposed in mdtests as an instance of + /// `ty_extensions.GenericContext`. + GenericContext(GenericContext<'db>), + + /// A specialization, which is exposed in mdtests as an instance of + /// `ty_extensions.Specialization`. + Specialization(Specialization<'db>), + + /// A single instance of `types.UnionType`, which stores the elements of + /// a PEP 604 union, or a `typing.Union`. + UnionType(UnionTypeInstance<'db>), + + /// A single instance of `typing.Literal` + Literal(InternedType<'db>), + + /// A single instance of `typing.Annotated` + Annotated(InternedType<'db>), + + /// An instance of `typing.GenericAlias` representing a `type[...]` expression. + TypeGenericAlias(InternedType<'db>), + + /// An instance of `typing.GenericAlias` representing a `Callable[...]` expression. + Callable(CallableType<'db>), + + /// A literal string which is the right-hand side of a PEP 613 `TypeAlias`. + LiteralStringAlias(InternedType<'db>), + + /// An identity callable created with `typing.NewType(name, base)`, which behaves like a + /// subtype of `base` in type expressions. See the `struct NewType` payload for an example. + NewType(NewType<'db>), + + /// The inferred spec for a functional `NamedTuple` class. + NamedTupleSpec(NamedTupleSpec<'db>), +} + +pub(super) fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + known_instance: KnownInstanceType<'db>, + visitor: &V, +) { + match known_instance { + KnownInstanceType::SubscriptedProtocol(context) + | KnownInstanceType::SubscriptedGeneric(context) => { + walk_generic_context(db, context, visitor); + } + KnownInstanceType::TypeVar(typevar) => { + visitor.visit_type_var_type(db, typevar); + } + KnownInstanceType::TypeAliasType(type_alias) => { + visitor.visit_type_alias_type(db, type_alias); + } + KnownInstanceType::Deprecated(_) + | KnownInstanceType::ConstraintSet(_) + | KnownInstanceType::GenericContext(_) + | KnownInstanceType::Specialization(_) => { + // Nothing to visit + } + KnownInstanceType::Field(field) => { + if let Some(default_ty) = field.default_type(db) { + visitor.visit_type(db, default_ty); + } + } + KnownInstanceType::UnionType(instance) => { + if let Ok(union_type) = instance.union_type(db) { + visitor.visit_type(db, *union_type); + } + } + KnownInstanceType::Literal(ty) + | KnownInstanceType::Annotated(ty) + | KnownInstanceType::TypeGenericAlias(ty) + | KnownInstanceType::LiteralStringAlias(ty) => { + visitor.visit_type(db, ty.inner(db)); + } + KnownInstanceType::Callable(callable) => { + visitor.visit_callable_type(db, callable); + } + KnownInstanceType::NewType(newtype) => { + visitor.visit_type(db, newtype.concrete_base_type(db)); + } + KnownInstanceType::NamedTupleSpec(spec) => { + for field in spec.fields(db) { + visitor.visit_type(db, field.ty); + } + } + } +} + +impl<'db> VarianceInferable<'db> for KnownInstanceType<'db> { + fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { + match self { + KnownInstanceType::TypeAliasType(type_alias) => { + type_alias.raw_value_type(db).variance_of(db, typevar) + } + _ => TypeVarVariance::Bivariant, + } + } +} + +impl<'db> KnownInstanceType<'db> { + pub(super) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + match self { + // Nothing to normalize + Self::SubscriptedProtocol(context) => Some(Self::SubscriptedProtocol(context)), + Self::SubscriptedGeneric(context) => Some(Self::SubscriptedGeneric(context)), + Self::Deprecated(deprecated) => Some(Self::Deprecated(deprecated)), + Self::ConstraintSet(set) => Some(Self::ConstraintSet(set)), + Self::TypeVar(typevar) => Some(Self::TypeVar(typevar)), + Self::TypeAliasType(type_alias) => Some(Self::TypeAliasType(type_alias)), + Self::Field(field) => field + .recursive_type_normalized_impl(db, div, nested) + .map(Self::Field), + Self::UnionType(union_type) => union_type + .recursive_type_normalized_impl(db, div, nested) + .map(Self::UnionType), + Self::Literal(ty) => ty + .recursive_type_normalized_impl(db, div, true) + .map(Self::Literal), + Self::Annotated(ty) => ty + .recursive_type_normalized_impl(db, div, true) + .map(Self::Annotated), + Self::TypeGenericAlias(ty) => ty + .recursive_type_normalized_impl(db, div, true) + .map(Self::TypeGenericAlias), + Self::LiteralStringAlias(ty) => ty + .recursive_type_normalized_impl(db, div, true) + .map(Self::LiteralStringAlias), + Self::Callable(callable) => callable + .recursive_type_normalized_impl(db, div, nested) + .map(Self::Callable), + Self::NewType(newtype) => newtype + .try_map_base_class_type(db, |class_type| { + class_type.recursive_type_normalized_impl(db, div, true) + }) + .map(Self::NewType), + Self::GenericContext(generic) => Some(Self::GenericContext(generic)), + Self::Specialization(specialization) => specialization + .recursive_type_normalized_impl(db, div, true) + .map(Self::Specialization), + Self::NamedTupleSpec(spec) => spec + .recursive_type_normalized_impl(db, div, true) + .map(Self::NamedTupleSpec), + } + } + + pub(super) fn class(self, db: &'db dyn Db) -> KnownClass { + match self { + Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm, + Self::TypeVar(typevar_instance) if typevar_instance.is_paramspec(db) => { + KnownClass::ParamSpec + } + Self::TypeVar(_) => KnownClass::TypeVar, + Self::TypeAliasType(TypeAliasType::PEP695(alias)) if alias.is_specialized(db) => { + KnownClass::GenericAlias + } + Self::TypeAliasType(_) => KnownClass::TypeAliasType, + Self::Deprecated(_) => KnownClass::Deprecated, + Self::Field(_) => KnownClass::Field, + Self::ConstraintSet(_) => KnownClass::ConstraintSet, + Self::GenericContext(_) => KnownClass::GenericContext, + Self::Specialization(_) => KnownClass::Specialization, + Self::UnionType(_) => KnownClass::UnionType, + Self::Literal(_) + | Self::Annotated(_) + | Self::TypeGenericAlias(_) + | Self::Callable(_) => KnownClass::GenericAlias, + Self::LiteralStringAlias(_) => KnownClass::Str, + Self::NewType(_) => KnownClass::NewType, + Self::NamedTupleSpec(_) => KnownClass::Sequence, + } + } + + pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + self.class(db).to_class_literal(db) + } + + /// Return the instance type which this type is a subtype of. + /// + /// For example, an alias created using the `type` statement is an instance of + /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db)` + /// returns `Type::NominalInstance(NominalInstanceType { class: })`. + pub(super) fn instance_fallback(self, db: &dyn Db) -> Type<'_> { + self.class(db).to_instance(db) + } + + /// Return `true` if this symbol is an instance of `class`. + pub(super) fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { + self.class(db).is_subclass_of(db, class) + } + + /// Return the repr of the symbol at runtime + pub(super) fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { + self.display_with(db, DisplaySettings::default()) + } + + pub(super) fn apply_type_mapping_impl( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'_, 'db>, + tcx: TypeContext<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Type<'db> { + match self { + KnownInstanceType::TypeVar(typevar) => match type_mapping { + TypeMapping::BindLegacyTypevars(binding_context) => Type::TypeVar( + BoundTypeVarInstance::new(db, typevar, *binding_context, None), + ), + TypeMapping::ApplySpecialization(_) + | TypeMapping::UniqueSpecialization { .. } + | TypeMapping::PromoteLiterals(_) + | TypeMapping::BindSelf(..) + | TypeMapping::ReplaceSelf { .. } + | TypeMapping::Materialize(_) + | TypeMapping::ReplaceParameterDefaults + | TypeMapping::EagerExpansion + | TypeMapping::RescopeReturnCallables(_) => Type::KnownInstance(self), + }, + KnownInstanceType::UnionType(instance) => { + Type::KnownInstance(KnownInstanceType::UnionType( + instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + )) + } + KnownInstanceType::Annotated(ty) => { + Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( + db, + ty.inner(db) + .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + ))) + } + KnownInstanceType::Callable(callable_type) => { + Type::KnownInstance(KnownInstanceType::Callable( + callable_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + )) + } + KnownInstanceType::TypeGenericAlias(ty) => { + Type::KnownInstance(KnownInstanceType::TypeGenericAlias(InternedType::new( + db, + ty.inner(db) + .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + ))) + } + + KnownInstanceType::SubscriptedProtocol(_) + | KnownInstanceType::SubscriptedGeneric(_) + | KnownInstanceType::TypeAliasType(_) + | KnownInstanceType::Deprecated(_) + | KnownInstanceType::Field(_) + | KnownInstanceType::ConstraintSet(_) + | KnownInstanceType::GenericContext(_) + | KnownInstanceType::Specialization(_) + | KnownInstanceType::Literal(_) + | KnownInstanceType::LiteralStringAlias(_) + | KnownInstanceType::NamedTupleSpec(_) + | KnownInstanceType::NewType(_) => { + // TODO: For some of these, we may need to apply the type mapping to inner types. + Type::KnownInstance(self) + } + } + } +} + +/// Data regarding a `warnings.deprecated` or `typing_extensions.deprecated` decorator. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct DeprecatedInstance<'db> { + /// The message for the deprecation + pub message: Option>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for DeprecatedInstance<'_> {} + +/// Contains information about instances of `dataclasses.Field`, typically created using +/// `dataclasses.field()`. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct FieldInstance<'db> { + /// The type of the default value for this field. This is derived from the `default` or + /// `default_factory` arguments to `dataclasses.field()`. + pub default_type: Option>, + + /// Whether this field is part of the `__init__` signature, or not. + pub init: bool, + + /// Whether or not this field can only be passed as a keyword argument to `__init__`. + pub kw_only: Option, + + /// This name is used to provide an alternative parameter name in the synthesized `__init__` method. + pub alias: Option>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for FieldInstance<'_> {} + +impl<'db> FieldInstance<'db> { + fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + let default_type = match self.default_type(db) { + Some(default) if nested => Some(default.recursive_type_normalized_impl(db, div, true)?), + Some(default) => Some( + default + .recursive_type_normalized_impl(db, div, true) + .unwrap_or(div), + ), + None => None, + }; + Some(FieldInstance::new( + db, + default_type, + self.init(db), + self.kw_only(db), + self.alias(db), + )) + } +} + +/// Contains information about a `types.UnionType` instance built from a PEP 604 +/// union or a legacy `typing.Union[…]` annotation in a value expression context, +/// e.g. `IntOrStr = int | str` or `IntOrStr = Union[int, str]`. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct UnionTypeInstance<'db> { + /// You probably don't want to access this field outside `UnionTypeInstance` + /// internals. + /// + /// This field is the types of the elements of this union, as they were inferred + /// in a value expression context. For `int | str`, this would contain + /// `` and ``. For `Union[int, str]`, this field is + /// `None`, as we infer the elements as type expressions. + /// + /// Use `value_expression_types` to get the corresponding value expression types. + #[returns(ref)] + _value_expr_types: Option<[Type<'db>; 2]>, + + /// The type of the full union, which can be used when this `UnionType` instance + /// is used in a type expression context. For `int | str`, this would contain + /// `Ok(int | str)`. If any of the element types could not be converted, this + /// contains the first encountered error. + #[returns(ref)] + pub(super) union_type: Result, InvalidTypeExpressionError<'db>>, +} + +impl get_size2::GetSize for UnionTypeInstance<'_> {} + +impl<'db> UnionTypeInstance<'db> { + pub(crate) fn from_value_expression_types( + db: &'db dyn Db, + value_expr_types: [Type<'db>; 2], + scope_id: ScopeId<'db>, + typevar_binding_context: Option>, + ) -> Type<'db> { + let mut builder = UnionBuilder::new(db); + for ty in &value_expr_types { + match ty.in_type_expression(db, scope_id, typevar_binding_context) { + Ok(ty) => builder.add_in_place(ty), + Err(error) => { + return Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new(db, Some(value_expr_types), Err(error)), + )); + } + } + } + + Type::KnownInstance(KnownInstanceType::UnionType(UnionTypeInstance::new( + db, + Some(value_expr_types), + Ok(builder.build()), + ))) + } + + pub(super) fn apply_type_mapping_impl( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'_, 'db>, + tcx: TypeContext<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + if let Ok(union_type) = self.union_type(db) { + UnionTypeInstance::new( + db, + self._value_expr_types(db), + Ok(union_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + ) + } else { + self + } + } + + /// Get the types of the elements of this union as they would appear in a value + /// expression context. For a PEP 604 union, we return the actual types that were + /// inferred when we encountered the union in a value expression context. For a + /// legacy `typing.Union[…]` annotation, we turn the type-expression types into + /// their corresponding value-expression types, i.e. we turn instances like `int` + /// into class literals like ``. This operation is potentially lossy. + pub(crate) fn value_expression_types( + self, + db: &'db dyn Db, + ) -> Result> + 'db, InvalidTypeExpressionError<'db>> { + let to_class_literal = |ty: Type<'db>| { + ty.as_nominal_instance() + .and_then(|instance| { + instance + .class(db) + .static_class_literal(db) + .map(|(lit, _)| Type::ClassLiteral(lit.into())) + }) + .unwrap_or_else(Type::unknown) + }; + + if let Some(value_expr_types) = self._value_expr_types(db) { + Ok(Either::Left(value_expr_types.iter().copied())) + } else { + match self.union_type(db).clone()? { + Type::Union(union) => Ok(Either::Right(Either::Left( + union.elements(db).iter().copied().map(to_class_literal), + ))), + ty => Ok(Either::Right(Either::Right(std::iter::once( + to_class_literal(ty), + )))), + } + } + } + + fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + // The `Divergent` elimination rules are different within union types. + // See `UnionType::recursive_type_normalized_impl` for details. + let value_expr_types = match self._value_expr_types(db).as_ref() { + Some([first, second]) if nested => Some([ + first.recursive_type_normalized_impl(db, div, nested)?, + second.recursive_type_normalized_impl(db, div, nested)?, + ]), + Some([first, second]) => Some([ + first + .recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div), + second + .recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div), + ]), + None => None, + }; + let union_type = match self.union_type(db).clone() { + Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, div, nested)?), + Ok(ty) => Ok(ty + .recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div)), + Err(err) => Err(err), + }; + + Some(Self::new(db, value_expr_types, union_type)) + } +} + +/// A salsa-interned `Type` +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct InternedType<'db> { + pub(super) inner: Type<'db>, +} + +impl get_size2::GetSize for InternedType<'_> {} + +impl<'db> InternedType<'db> { + fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + let inner = if nested { + self.inner(db) + .recursive_type_normalized_impl(db, div, nested)? + } else { + self.inner(db) + .recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div) + }; + Some(InternedType::new(db, inner)) + } +} diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index cccfa2ed6e967..002a81e828ef1 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -11,12 +11,13 @@ use crate::{ class::walk_generic_alias, function::{FunctionType, walk_function_type}, instance::{walk_nominal_instance_type, walk_protocol_instance_type}, + known_instance::walk_known_instance_type, newtype::{NewType, walk_newtype_instance_type}, subclass_of::walk_subclass_of_type, walk_bound_method_type, walk_bound_type_var_type, walk_callable_type, - walk_intersection_type, walk_known_instance_type, walk_method_wrapper_type, - walk_property_instance_type, walk_type_alias_type, walk_type_var_type, - walk_typed_dict_type, walk_typeguard_type, walk_typeis_type, walk_union, + walk_intersection_type, walk_method_wrapper_type, walk_property_instance_type, + walk_type_alias_type, walk_type_var_type, walk_typed_dict_type, walk_typeguard_type, + walk_typeis_type, walk_union, }, }; use std::cell::{Cell, RefCell}; From 48cf24d48bbf59fb8b236087e0966291a07674b9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Mar 2026 18:05:00 +0000 Subject: [PATCH 173/261] [ty] Introduce `types::bool`, `types::context_manager` and `types::iteration` (#23681) ## Summary And move relevant methods to those submodules ## Test Plan existing tests --- .../src/semantic_index/builder.rs | 4 +- crates/ty_python_semantic/src/types.rs | 1555 +---------------- crates/ty_python_semantic/src/types/bool.rs | 487 ++++++ .../ty_python_semantic/src/types/call/bind.rs | 11 +- .../src/types/context_manager.rs | 257 +++ .../src/types/infer/builder.rs | 6 +- .../ty_python_semantic/src/types/iteration.rs | 822 +++++++++ crates/ty_python_semantic/src/unpack.rs | 21 +- 8 files changed, 1604 insertions(+), 1559 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/bool.rs create mode 100644 crates/ty_python_semantic/src/types/context_manager.rs create mode 100644 crates/ty_python_semantic/src/types/iteration.rs diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 17fa4147f0625..71645d77565e8 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -56,8 +56,8 @@ use crate::semantic_index::{ get_loop_header, }; use crate::semantic_model::HasTrackedScope; -use crate::types::PossiblyNarrowedPlaces; -use crate::unpack::{EvaluationMode, Unpack, UnpackKind, UnpackPosition, UnpackValue}; +use crate::types::{EvaluationMode, PossiblyNarrowedPlaces}; +use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue}; use crate::{Db, Program}; mod except_handlers; diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e1304f3260569..0963ad40de8ca 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -11,14 +11,13 @@ use std::time::Duration; use bitflags::bitflags; use call::{CallDunderError, CallError, CallErrorKind}; use context::InferContext; -use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE}; use ruff_db::Instant; -use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; +use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::Ranged; use smallvec::{SmallVec, smallvec_inline}; use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; @@ -57,7 +56,7 @@ use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; -use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; +use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM}; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; use crate::types::enums::{enum_metadata, is_single_member_enum}; use crate::types::function::{ @@ -78,13 +77,11 @@ use crate::types::newtype::NewType; pub(crate) use crate::types::signatures::{Parameter, Parameters}; use crate::types::signatures::{ParameterForm, walk_signature}; use crate::types::special_form::TypeQualifier; -use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder}; -use crate::types::typed_dict::TypedDictField; +use crate::types::tuple::{Tuple, TupleSpec}; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; pub use crate::types::variance::TypeVarVariance; use crate::types::variance::VarianceInferable; use crate::types::visitor::any_over_type; -use crate::unpack::EvaluationMode; use crate::{Db, FxOrderSet, Program}; pub use class::KnownClass; pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, StaticClassLiteral}; @@ -95,12 +92,14 @@ pub(crate) use literal::{ }; pub use special_form::SpecialFormType; +mod bool; mod bound_super; mod call; mod class; mod class_base; mod constraints; mod context; +mod context_manager; mod cyclic; mod diagnostic; mod display; @@ -110,6 +109,7 @@ mod generics; pub mod ide_support; mod infer; mod instance; +mod iteration; mod known_instance; pub mod list_members; mod literal; @@ -232,11 +232,6 @@ pub(crate) type FindLegacyTypeVarsVisitor<'db> = CycleDetector = - CycleDetector, Result>>; -pub(crate) struct TryBool; - /// A [`CycleDetector`] that is used in `visit_specialization` methods. pub(crate) type SpecializationVisitor<'db> = CycleDetector, ()>; pub(crate) struct VisitSpecialization; @@ -3527,310 +3522,6 @@ impl<'db> Type<'db> { } } - /// Resolves the boolean value of the type and falls back to [`Truthiness::Ambiguous`] if the type doesn't implement `__bool__` correctly. - /// - /// This method should only be used outside type checking or when evaluating if a type - /// is truthy or falsy in a context where Python doesn't make an implicit `bool` call. - /// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls. - pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness { - self.try_bool_impl(db, true, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) - .unwrap_or_else(|err| err.fallback_truthiness()) - } - - /// Resolves the boolean value of a type. - /// - /// This is used to determine the value that would be returned - /// when `bool(x)` is called on an object `x`. - /// - /// Returns an error if the type doesn't implement `__bool__` correctly. - pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result> { - self.try_bool_impl(db, false, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) - } - - /// Resolves the boolean value of a type. - /// - /// Setting `allow_short_circuit` to `true` allows the implementation to - /// early return if the bool value of any union variant is `Truthiness::Ambiguous`. - /// Early returning shows a 1-2% perf improvement on our benchmarks because - /// `bool` (which doesn't care about errors) is used heavily when evaluating statically known branches. - /// - /// An alternative to this flag is to implement a trait similar to Rust's `Try` trait. - /// The advantage of that is that it would allow collecting the errors as well. However, - /// it is significantly more complex and duplicating the logic into `bool` without the error - /// handling didn't show any significant performance difference to when using the `allow_short_circuit` flag. - #[inline] - fn try_bool_impl( - &self, - db: &'db dyn Db, - allow_short_circuit: bool, - visitor: &TryBoolVisitor<'db>, - ) -> Result> { - let type_to_truthiness = |ty: Type<'db>| { - match ty.as_literal_value_kind() { - Some(LiteralValueTypeKind::Bool(bool_val)) => Truthiness::from(bool_val), - Some(LiteralValueTypeKind::Int(int_val)) => Truthiness::from(int_val.as_i64() != 0), - // anything else is handled lower down - _ => Truthiness::Ambiguous, - } - }; - - let try_dunders = || { - match self.try_call_dunder( - db, - "__bool__", - CallArguments::none(), - TypeContext::default(), - ) { - Ok(outcome) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { - // The type has a `__bool__` method, but it doesn't return a - // boolean. - return Err(BoolError::IncorrectReturnType { - return_type, - not_boolable_type: *self, - }); - } - Ok(type_to_truthiness(return_type)) - } - - Err(CallDunderError::PossiblyUnbound(outcome)) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { - // The type has a `__bool__` method, but it doesn't return a - // boolean. - return Err(BoolError::IncorrectReturnType { - return_type: outcome.return_type(db), - not_boolable_type: *self, - }); - } - - // Don't trust possibly missing `__bool__` method. - Ok(Truthiness::Ambiguous) - } - - Err(CallDunderError::MethodNotAvailable) => { - // We only consider `__len__` for tuples and `@final` types, - // since `__bool__` takes precedence - // and a subclass could add a `__bool__` method. - // - // TODO: with regards to tuple types, we intend to emit a diagnostic - // if a tuple subclass defines a `__bool__` method with a return type - // that is inconsistent with the tuple's length. Otherwise, the special - // handling for tuples here isn't sound. - if let Some(instance) = self.as_nominal_instance() { - if let Some(tuple_spec) = instance.tuple_spec(db) { - Ok(tuple_spec.truthiness()) - } else if instance.class(db).is_final(db) { - match self.try_call_dunder( - db, - "__len__", - CallArguments::none(), - TypeContext::default(), - ) { - Ok(outcome) => { - let return_type = outcome.return_type(db); - if return_type.is_assignable_to( - db, - KnownClass::SupportsIndex.to_instance(db), - ) { - Ok(type_to_truthiness(return_type)) - } else { - // TODO: should report a diagnostic similar to if return type of `__bool__` - // is not assignable to `bool` - Ok(Truthiness::Ambiguous) - } - } - // if a `@final` type does not define `__bool__` or `__len__`, it is always truthy - Err(CallDunderError::MethodNotAvailable) => { - Ok(Truthiness::AlwaysTrue) - } - // TODO: errors during a `__len__` call (if `__len__` exists) should be reported - // as diagnostics similar to errors during a `__bool__` call (when `__bool__` exists) - Err(_) => Ok(Truthiness::Ambiguous), - } - } else { - Ok(Truthiness::Ambiguous) - } - } else { - Ok(Truthiness::Ambiguous) - } - } - - Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => { - Err(BoolError::IncorrectArguments { - truthiness: type_to_truthiness(bindings.return_type(db)), - not_boolable_type: *self, - }) - } - - Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => { - Err(BoolError::NotCallable { - not_boolable_type: *self, - }) - } - - Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => { - Err(BoolError::Other { - not_boolable_type: *self, - }) - } - } - }; - - let try_union = |union: UnionType<'db>| { - let mut truthiness = None; - let mut all_not_callable = true; - let mut has_errors = false; - - for element in union.elements(db) { - let element_truthiness = - match element.try_bool_impl(db, allow_short_circuit, visitor) { - Ok(truthiness) => truthiness, - Err(err) => { - has_errors = true; - all_not_callable &= matches!(err, BoolError::NotCallable { .. }); - err.fallback_truthiness() - } - }; - - truthiness.get_or_insert(element_truthiness); - - if Some(element_truthiness) != truthiness { - truthiness = Some(Truthiness::Ambiguous); - - if allow_short_circuit { - return Ok(Truthiness::Ambiguous); - } - } - } - - if has_errors { - if all_not_callable { - return Err(BoolError::NotCallable { - not_boolable_type: *self, - }); - } - return Err(BoolError::Union { - union, - truthiness: truthiness.unwrap_or(Truthiness::Ambiguous), - }); - } - Ok(truthiness.unwrap_or(Truthiness::Ambiguous)) - }; - - let truthiness = match self { - Type::Dynamic(_) - | Type::Never - | Type::Callable(_) - | Type::TypeIs(_) - | Type::TypeGuard(_) => Truthiness::Ambiguous, - - Type::TypedDict(td) => { - if td.items(db).values().any(TypedDictField::is_required) { - Truthiness::AlwaysTrue - } else { - // We can potentially infer empty typeddicts as always falsy if they're `closed=True`, - // but as of 22-01-26 we don't yet support PEP 728. - Truthiness::Ambiguous - } - } - - Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => { - let constraints = ConstraintSetBuilder::new(); - let tracked_set = constraints.load(tracked_set.constraints(db)); - Truthiness::from(tracked_set.is_always_satisfied(db)) - } - - Type::FunctionLiteral(_) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::KnownBoundMethod(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::ModuleLiteral(_) - | Type::PropertyInstance(_) - | Type::BoundSuper(_) - | Type::KnownInstance(_) - | Type::SpecialForm(_) - | Type::AlwaysTruthy => Truthiness::AlwaysTrue, - - Type::AlwaysFalsy => Truthiness::AlwaysFalse, - - Type::ClassLiteral(class) => { - class - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)? - } - Type::GenericAlias(alias) => ClassType::from(*alias) - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, - - Type::SubclassOf(subclass_of_ty) => { - match subclass_of_ty.subclass_of().with_transposed_type_var(db) { - SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, - SubclassOfInner::Class(class) => { - Type::from(class).try_bool_impl(db, allow_short_circuit, visitor)? - } - SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar) - .try_bool_impl(db, allow_short_circuit, visitor)?, - } - } - - Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => Truthiness::Ambiguous, - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.try_bool_impl(db, allow_short_circuit, visitor)? - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .as_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, - } - } - - Type::NominalInstance(instance) => instance - .known_class(db) - .and_then(KnownClass::bool) - .map(Ok) - .unwrap_or_else(try_dunders)?, - - Type::ProtocolInstance(_) => try_dunders()?, - - Type::Union(union) => try_union(*union)?, - - Type::Intersection(_) => { - // TODO - Truthiness::Ambiguous - } - - Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::LiteralString => Truthiness::Ambiguous, - LiteralValueTypeKind::Enum(enum_type) => enum_type - .enum_class_instance(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, - - LiteralValueTypeKind::Int(num) => Truthiness::from(num.as_i64() != 0), - LiteralValueTypeKind::Bool(bool) => Truthiness::from(bool), - LiteralValueTypeKind::String(str) => Truthiness::from(!str.value(db).is_empty()), - LiteralValueTypeKind::Bytes(bytes) => Truthiness::from(!bytes.value(db).is_empty()), - }, - - Type::TypeAlias(alias) => visitor.visit(*self, || { - alias - .value_type(db) - .try_bool_impl(db, allow_short_circuit, visitor) - })?, - Type::NewTypeInstance(newtype) => { - newtype - .concrete_base_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)? - } - }; - - Ok(truthiness) - } - /// Return the type of `len()` on a type if it is known more precisely than `int`, /// or `None` otherwise. /// @@ -5243,433 +4934,6 @@ impl<'db> Type<'db> { } } - /// Returns a tuple spec describing the elements that are produced when iterating over `self`. - /// - /// This method should only be used outside of type checking because it omits any errors. - /// For type checking, use [`try_iterate`](Self::try_iterate) instead. - fn iterate(self, db: &'db dyn Db) -> Cow<'db, TupleSpec<'db>> { - self.try_iterate(db) - .unwrap_or_else(|err| Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db)))) - } - - /// Given the type of an object that is iterated over in some way, - /// return a tuple spec describing the type of objects that are yielded by that iteration. - /// - /// E.g., for the following call, given the type of `x`, infer the types of the values that are - /// splatted into `y`'s positional arguments: - /// ```python - /// y(*x) - /// ``` - fn try_iterate(self, db: &'db dyn Db) -> Result>, IterationError<'db>> { - self.try_iterate_with_mode(db, EvaluationMode::Sync) - } - - fn try_iterate_with_mode( - self, - db: &'db dyn Db, - mode: EvaluationMode, - ) -> Result>, IterationError<'db>> { - fn non_async_special_case<'db>( - db: &'db dyn Db, - ty: Type<'db>, - ) -> Option>> { - // We will not infer precise heterogeneous tuple specs for literals with lengths above this threshold. - // The threshold here is somewhat arbitrary and conservative; it could be increased if needed. - // However, it's probably very rare to need heterogeneous unpacking inference for long string literals - // or bytes literals, and creating long heterogeneous tuple specs has a performance cost. - const MAX_TUPLE_LENGTH: usize = 128; - - match ty { - Type::NominalInstance(nominal) => nominal.tuple_spec(db), - Type::NewTypeInstance(newtype) => non_async_special_case(db, newtype.concrete_base_type(db)), - Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => { - Some(Cow::Owned(TupleSpec::homogeneous(todo_type!( - "*tuple[] annotations" - )))) - } - Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::Bytes(bytes) => { - let bytes_literal = bytes.value(db); - let spec = if bytes_literal.len() < MAX_TUPLE_LENGTH { - TupleSpec::heterogeneous( - bytes_literal - .iter() - .map(|b| Type::int_literal( i64::from(*b))), - ) - } else { - TupleSpec::homogeneous(KnownClass::Int.to_instance(db)) - }; - Some(Cow::Owned(spec)) - }, - LiteralValueTypeKind::String(string_literal_ty) => { - let string_literal = string_literal_ty.value(db); - let spec = if string_literal.len() < MAX_TUPLE_LENGTH { - TupleSpec::heterogeneous( - string_literal - .chars() - .map(|c| Type::string_literal(db, &c.to_string())), - ) - } else { - TupleSpec::homogeneous(Type::literal_string()) - }; - Some(Cow::Owned(spec)) - } - // N.B. This special case isn't strictly necessary, it's just an obvious optimization - LiteralValueTypeKind::LiteralString => { - Some(Cow::Owned(TupleSpec::homogeneous(ty))) - } - _ => None - } - Type::Never => { - // The dunder logic below would have us return `tuple[Never, ...]`, which eagerly - // simplifies to `tuple[()]`. That will will cause us to emit false positives if we - // index into the tuple. Using `tuple[Unknown, ...]` avoids these false positives. - // TODO: Consider removing this special case, and instead hide the indexing - // diagnostic in unreachable code. - Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))) - } - Type::TypeAlias(alias) => { - non_async_special_case(db, alias.value_type(db)) - } - Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db)? { - TypeVarBoundOrConstraints::UpperBound(bound) => { - non_async_special_case(db, bound) - } - TypeVarBoundOrConstraints::Constraints(constraints) => non_async_special_case(db, constraints.as_type(db)), - }, - Type::Union(union) => { - let elements = union.elements(db); - if elements.len() < MAX_TUPLE_LENGTH { - let mut elements_iter = elements.iter(); - let first_element_spec = elements_iter.next()?.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?; - let mut builder = TupleSpecBuilder::from(&*first_element_spec); - for element in elements_iter { - builder = builder.union(db, &*element.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?); - } - Some(Cow::Owned(builder.build())) - } else { - None - } - } - Type::Intersection(intersection) => { - // For intersections containing TypeVars with union bounds, we need to - // flatten the TypeVars first. This distributes the intersection over - // the union and simplifies, e.g.: - // `T & tuple[object, ...]` where `T: tuple[int, ...] | list[str]` - // becomes `(tuple[int, ...] & tuple[object, ...]) | (list[str] & tuple[object, ...])` - // which simplifies to `tuple[int, ...] | Never` = `tuple[int, ...]` - // - // After flattening, the result may be: - // - An intersection (if no union-bound typevars, or they didn't simplify). - // - A union of intersections (if distribution happened). - // - A simpler type (if it fully simplified). - // - // We then iterate over the flattened type. - let flattened = ty.flatten_typevars(db); - - // If flattening didn't change anything, iterate the intersection directly. - if flattened == ty { - let mut specs_iter = intersection.positive_elements_or_object(db).filter_map( - |element| element.try_iterate_with_mode(db, EvaluationMode::Sync).ok(), - ); - let first_spec = specs_iter.next()?; - let mut builder = TupleSpecBuilder::from(&*first_spec); - for spec in specs_iter { - // Two tuples cannot have incompatible specs unless the tuples themselves - // are disjoint. `IntersectionBuilder` eagerly simplifies such - // intersections to `Never`, so this should always return `Some`. - let Some(intersected) = builder.intersect(db, &spec) else { - return Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))); - }; - builder = intersected; - } - return Some(Cow::Owned(builder.build())); - } - - // Flattening changed the type; recursively iterate the flattened result. - non_async_special_case(db, flattened) - } - // N.B. This special case isn't strictly necessary, it's just an obvious optimization - Type::Dynamic(_) => Some(Cow::Owned(TupleSpec::homogeneous(ty))), - - Type::FunctionLiteral(_) - | Type::GenericAlias(_) - | Type::BoundMethod(_) - | Type::KnownBoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::Callable(_) - | Type::ModuleLiteral(_) - // We could infer a precise tuple spec for enum classes with members, - // but it's not clear whether that's worth the added complexity: - // you'd have to check that `EnumMeta.__iter__` is not overridden for it to be sound - // (enums can have `EnumMeta` subclasses as their metaclasses). - | Type::ClassLiteral(_) - | Type::SubclassOf(_) - | Type::ProtocolInstance(_) - | Type::SpecialForm(_) - | Type::KnownInstance(_) - | Type::PropertyInstance(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::BoundSuper(_) - | Type::TypeIs(_) - | Type::TypeGuard(_) - | Type::TypedDict(_) => None - } - } - - if mode.is_async() { - let try_call_dunder_anext_on_iterator = |iterator: Type<'db>| -> Result< - Result, AwaitError<'db>>, - CallDunderError<'db>, - > { - iterator - .try_call_dunder( - db, - "__anext__", - CallArguments::none(), - TypeContext::default(), - ) - .map(|dunder_anext_outcome| dunder_anext_outcome.return_type(db).try_await(db)) - }; - - return match self.try_call_dunder( - db, - "__aiter__", - CallArguments::none(), - TypeContext::default(), - ) { - Ok(dunder_aiter_bindings) => { - let iterator = dunder_aiter_bindings.return_type(db); - match try_call_dunder_anext_on_iterator(iterator) { - Ok(Ok(result)) => Ok(Cow::Owned(TupleSpec::homogeneous(result))), - Ok(Err(AwaitError::InvalidReturnType(..))) => { - Err(IterationError::UnboundAiterError) - } // TODO: __anext__ is bound, but is not properly awaitable - Err(dunder_anext_error) | Ok(Err(AwaitError::Call(dunder_anext_error))) => { - Err(IterationError::IterReturnsInvalidIterator { - iterator, - dunder_error: dunder_anext_error, - mode, - }) - } - } - } - Err(CallDunderError::PossiblyUnbound(dunder_aiter_bindings)) => { - let iterator = dunder_aiter_bindings.return_type(db); - match try_call_dunder_anext_on_iterator(iterator) { - Ok(_) => Err(IterationError::IterCallError { - kind: CallErrorKind::PossiblyNotCallable, - bindings: dunder_aiter_bindings, - mode, - }), - Err(dunder_anext_error) => { - Err(IterationError::IterReturnsInvalidIterator { - iterator, - dunder_error: dunder_anext_error, - mode, - }) - } - } - } - Err(CallDunderError::CallError(kind, bindings)) => { - Err(IterationError::IterCallError { - kind, - bindings, - mode, - }) - } - Err(CallDunderError::MethodNotAvailable) => Err(IterationError::UnboundAiterError), - }; - } - - if let Some(special_case) = non_async_special_case(db, self) { - return Ok(special_case); - } - - let try_call_dunder_getitem = || { - self.try_call_dunder( - db, - "__getitem__", - CallArguments::positional([KnownClass::Int.to_instance(db)]), - TypeContext::default(), - ) - .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db)) - }; - - let try_call_dunder_next_on_iterator = |iterator: Type<'db>| { - iterator - .try_call_dunder( - db, - "__next__", - CallArguments::none(), - TypeContext::default(), - ) - .map(|dunder_next_outcome| dunder_next_outcome.return_type(db)) - }; - - let dunder_iter_result = self - .try_call_dunder( - db, - "__iter__", - CallArguments::none(), - TypeContext::default(), - ) - .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db)); - - match dunder_iter_result { - Ok(iterator) => { - // `__iter__` is definitely bound and calling it succeeds. - // See what calling `__next__` on the object returned by `__iter__` gives us... - try_call_dunder_next_on_iterator(iterator) - .map(|ty| Cow::Owned(TupleSpec::homogeneous(ty))) - .map_err( - |dunder_next_error| IterationError::IterReturnsInvalidIterator { - iterator, - dunder_error: dunder_next_error, - mode, - }, - ) - } - - // `__iter__` is possibly unbound... - Err(CallDunderError::PossiblyUnbound(dunder_iter_outcome)) => { - let iterator = dunder_iter_outcome.return_type(db); - - match try_call_dunder_next_on_iterator(iterator) { - Ok(dunder_next_return) => { - try_call_dunder_getitem() - .map(|dunder_getitem_return_type| { - // If `__iter__` is possibly unbound, - // but it returns an object that has a bound and valid `__next__` method, - // *and* the object has a bound and valid `__getitem__` method, - // we infer a union of the type returned by the `__next__` method - // and the type returned by the `__getitem__` method. - // - // No diagnostic is emitted; iteration will always succeed! - Cow::Owned(TupleSpec::homogeneous(UnionType::from_two_elements( - db, - dunder_next_return, - dunder_getitem_return_type, - ))) - }) - .map_err(|dunder_getitem_error| { - IterationError::PossiblyUnboundIterAndGetitemError { - dunder_next_return, - dunder_getitem_error, - } - }) - } - - Err(dunder_next_error) => Err(IterationError::IterReturnsInvalidIterator { - iterator, - dunder_error: dunder_next_error, - mode, - }), - } - } - - // `__iter__` is definitely bound but it can't be called with the expected arguments - Err(CallDunderError::CallError(kind, bindings)) => Err(IterationError::IterCallError { - kind, - bindings, - mode, - }), - - // There's no `__iter__` method. Try `__getitem__` instead... - Err(CallDunderError::MethodNotAvailable) => try_call_dunder_getitem() - .map(|ty| Cow::Owned(TupleSpec::homogeneous(ty))) - .map_err( - |dunder_getitem_error| IterationError::UnboundIterAndGetitemError { - dunder_getitem_error, - }, - ), - } - } - - /// Returns the type bound from a context manager with type `self`. - /// - /// This method should only be used outside of type checking because it omits any errors. - /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. - fn enter(self, db: &'db dyn Db) -> Type<'db> { - self.try_enter_with_mode(db, EvaluationMode::Sync) - .unwrap_or_else(|err| err.fallback_enter_type(db)) - } - - /// Returns the type bound from a context manager with type `self`. - /// - /// This method should only be used outside of type checking because it omits any errors. - /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. - fn aenter(self, db: &'db dyn Db) -> Type<'db> { - self.try_enter_with_mode(db, EvaluationMode::Async) - .unwrap_or_else(|err| err.fallback_enter_type(db)) - } - - /// Given the type of an object that is used as a context manager (i.e. in a `with` statement), - /// return the return type of its `__enter__` or `__aenter__` method, which is bound to any potential targets. - /// - /// E.g., for the following `with` statement, given the type of `x`, infer the type of `y`: - /// ```python - /// with x as y: - /// pass - /// ``` - fn try_enter_with_mode( - self, - db: &'db dyn Db, - mode: EvaluationMode, - ) -> Result, ContextManagerError<'db>> { - let (enter_method, exit_method) = match mode { - EvaluationMode::Async => ("__aenter__", "__aexit__"), - EvaluationMode::Sync => ("__enter__", "__exit__"), - }; - - let enter = self.try_call_dunder( - db, - enter_method, - CallArguments::none(), - TypeContext::default(), - ); - let exit = self.try_call_dunder( - db, - exit_method, - CallArguments::positional([Type::none(db), Type::none(db), Type::none(db)]), - TypeContext::default(), - ); - - // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). - match (enter, exit) { - (Ok(enter), Ok(_)) => { - let ty = enter.return_type(db); - Ok(if mode.is_async() { - ty.try_await(db).unwrap_or(Type::unknown()) - } else { - ty - }) - } - (Ok(enter), Err(exit_error)) => { - let ty = enter.return_type(db); - Err(ContextManagerError::Exit { - enter_return_type: if mode.is_async() { - ty.try_await(db).unwrap_or(Type::unknown()) - } else { - ty - }, - exit_error, - mode, - }) - } - // TODO: Use the `exit_ty` to determine if any raised exception is suppressed. - (Err(enter_error), Ok(_)) => Err(ContextManagerError::Enter(enter_error, mode)), - (Err(enter_error), Err(exit_error)) => Err(ContextManagerError::EnterAndExit { - enter_error, - exit_error, - mode, - }), - } - } - /// Resolve the type of an `await …` expression where `self` is the type of the awaitable. fn try_await(self, db: &'db dyn Db) -> Result, AwaitError<'db>> { let await_result = self.try_call_dunder( @@ -8920,791 +8184,6 @@ impl<'db> AwaitError<'db> { } } -/// Error returned if a type is not (or may not be) a context manager. -#[derive(Debug)] -enum ContextManagerError<'db> { - Enter(CallDunderError<'db>, EvaluationMode), - Exit { - enter_return_type: Type<'db>, - exit_error: CallDunderError<'db>, - mode: EvaluationMode, - }, - EnterAndExit { - enter_error: CallDunderError<'db>, - exit_error: CallDunderError<'db>, - mode: EvaluationMode, - }, -} - -impl<'db> ContextManagerError<'db> { - fn fallback_enter_type(&self, db: &'db dyn Db) -> Type<'db> { - self.enter_type(db).unwrap_or(Type::unknown()) - } - - /// Returns the `__enter__` or `__aenter__` return type if it is known, - /// or `None` if the type never has a callable `__enter__` or `__aenter__` attribute - fn enter_type(&self, db: &'db dyn Db) -> Option> { - match self { - Self::Exit { - enter_return_type, - exit_error: _, - mode: _, - } => Some(*enter_return_type), - Self::Enter(enter_error, _) - | Self::EnterAndExit { - enter_error, - exit_error: _, - mode: _, - } => match enter_error { - CallDunderError::PossiblyUnbound(call_outcome) => { - Some(call_outcome.return_type(db)) - } - CallDunderError::CallError(CallErrorKind::NotCallable, _) => None, - CallDunderError::CallError(_, bindings) => Some(bindings.return_type(db)), - CallDunderError::MethodNotAvailable => None, - }, - } - } - - fn report_diagnostic( - &self, - context: &InferContext<'db, '_>, - context_expression_type: Type<'db>, - context_expression_node: ast::AnyNodeRef, - ) { - let Some(builder) = context.report_lint(&INVALID_CONTEXT_MANAGER, context_expression_node) - else { - return; - }; - - let mode = match self { - Self::Exit { mode, .. } | Self::Enter(_, mode) | Self::EnterAndExit { mode, .. } => { - *mode - } - }; - - let (enter_method, exit_method) = match mode { - EvaluationMode::Async => ("__aenter__", "__aexit__"), - EvaluationMode::Sync => ("__enter__", "__exit__"), - }; - - let format_call_dunder_error = |call_dunder_error: &CallDunderError<'db>, name: &str| { - match call_dunder_error { - CallDunderError::MethodNotAvailable => format!("it does not implement `{name}`"), - CallDunderError::PossiblyUnbound(_) => { - format!("the method `{name}` may be missing") - } - // TODO: Use more specific error messages for the different error cases. - // E.g. hint toward the union variant that doesn't correctly implement enter, - // distinguish between a not callable `__enter__` attribute and a wrong signature. - CallDunderError::CallError(_, _) => { - format!("it does not correctly implement `{name}`") - } - } - }; - - let format_call_dunder_errors = |error_a: &CallDunderError<'db>, - name_a: &str, - error_b: &CallDunderError<'db>, - name_b: &str| { - match (error_a, error_b) { - (CallDunderError::PossiblyUnbound(_), CallDunderError::PossiblyUnbound(_)) => { - format!("the methods `{name_a}` and `{name_b}` are possibly missing") - } - (CallDunderError::MethodNotAvailable, CallDunderError::MethodNotAvailable) => { - format!("it does not implement `{name_a}` and `{name_b}`") - } - (CallDunderError::CallError(_, _), CallDunderError::CallError(_, _)) => { - format!("it does not correctly implement `{name_a}` or `{name_b}`") - } - (_, _) => format!( - "{format_a}, and {format_b}", - format_a = format_call_dunder_error(error_a, name_a), - format_b = format_call_dunder_error(error_b, name_b) - ), - } - }; - - let db = context.db(); - - let formatted_errors = match self { - Self::Exit { - enter_return_type: _, - exit_error, - mode: _, - } => format_call_dunder_error(exit_error, exit_method), - Self::Enter(enter_error, _) => format_call_dunder_error(enter_error, enter_method), - Self::EnterAndExit { - enter_error, - exit_error, - mode: _, - } => format_call_dunder_errors(enter_error, enter_method, exit_error, exit_method), - }; - - // Suggest using `async with` if only async methods are available in a sync context, - // or suggest using `with` if only sync methods are available in an async context. - let with_kw = match mode { - EvaluationMode::Sync => "with", - EvaluationMode::Async => "async with", - }; - - let mut diag = builder.into_diagnostic(format_args!( - "Object of type `{}` cannot be used with `{}` because {}", - context_expression_type.display(db), - with_kw, - formatted_errors, - )); - - let (alt_mode, alt_enter_method, alt_exit_method, alt_with_kw) = match mode { - EvaluationMode::Sync => ("async", "__aenter__", "__aexit__", "async with"), - EvaluationMode::Async => ("sync", "__enter__", "__exit__", "with"), - }; - - let alt_enter = context_expression_type.try_call_dunder( - db, - alt_enter_method, - CallArguments::none(), - TypeContext::default(), - ); - let alt_exit = context_expression_type.try_call_dunder( - db, - alt_exit_method, - CallArguments::positional([Type::unknown(), Type::unknown(), Type::unknown()]), - TypeContext::default(), - ); - - if (alt_enter.is_ok() || matches!(alt_enter, Err(CallDunderError::CallError(..)))) - && (alt_exit.is_ok() || matches!(alt_exit, Err(CallDunderError::CallError(..)))) - { - diag.info(format_args!( - "Objects of type `{}` can be used as {} context managers", - context_expression_type.display(db), - alt_mode - )); - diag.info(format!("Consider using `{alt_with_kw}` here")); - } - } -} - -/// Error returned if a type is not (or may not be) iterable. -#[derive(Debug)] -enum IterationError<'db> { - /// The object being iterated over has a bound `__(a)iter__` method, - /// but calling it with the expected arguments results in an error. - IterCallError { - kind: CallErrorKind, - bindings: Box>, - mode: EvaluationMode, - }, - - /// The object being iterated over has a bound `__(a)iter__` method that can be called - /// with the expected types, but it returns an object that is not a valid iterator. - IterReturnsInvalidIterator { - /// The type of the object returned by the `__(a)iter__` method. - iterator: Type<'db>, - /// The error we encountered when we tried to call `__(a)next__` on the type - /// returned by `__(a)iter__` - dunder_error: CallDunderError<'db>, - /// Whether this is a synchronous or an asynchronous iterator. - mode: EvaluationMode, - }, - - /// The object being iterated over has a bound `__iter__` method that returns a - /// valid iterator. However, the `__iter__` method is possibly unbound, and there - /// either isn't a `__getitem__` method to fall back to, or calling the `__getitem__` - /// method returns some kind of error. - PossiblyUnboundIterAndGetitemError { - /// The type of the object returned by the `__next__` method on the iterator. - /// (The iterator being the type returned by the `__iter__` method on the iterable.) - dunder_next_return: Type<'db>, - /// The error we encountered when we tried to call `__getitem__` on the iterable. - dunder_getitem_error: CallDunderError<'db>, - }, - - /// The object being iterated over doesn't have an `__iter__` method. - /// It also either doesn't have a `__getitem__` method to fall back to, - /// or calling the `__getitem__` method returns some kind of error. - UnboundIterAndGetitemError { - dunder_getitem_error: CallDunderError<'db>, - }, - - /// The asynchronous iterable has no `__aiter__` method. - UnboundAiterError, -} - -impl<'db> IterationError<'db> { - fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { - self.element_type(db).unwrap_or(Type::unknown()) - } - - /// Returns the element type if it is known, or `None` if the type is never iterable. - fn element_type(&self, db: &'db dyn Db) -> Option> { - let return_type = |result: Result, CallDunderError<'db>>| { - result - .map(|outcome| Some(outcome.return_type(db))) - .unwrap_or_else(|call_error| call_error.return_type(db)) - }; - - match self { - Self::IterReturnsInvalidIterator { - dunder_error, mode, .. - } => dunder_error.return_type(db).and_then(|ty| { - if mode.is_async() { - ty.try_await(db).ok() - } else { - Some(ty) - } - }), - - Self::IterCallError { - kind: _, - bindings: dunder_iter_bindings, - mode, - } => { - if mode.is_async() { - return_type(dunder_iter_bindings.return_type(db).try_call_dunder( - db, - "__anext__", - CallArguments::none(), - TypeContext::default(), - )) - .and_then(|ty| ty.try_await(db).ok()) - } else { - return_type(dunder_iter_bindings.return_type(db).try_call_dunder( - db, - "__next__", - CallArguments::none(), - TypeContext::default(), - )) - } - } - - Self::PossiblyUnboundIterAndGetitemError { - dunder_next_return, - dunder_getitem_error, - } => match dunder_getitem_error { - CallDunderError::MethodNotAvailable => Some(*dunder_next_return), - CallDunderError::PossiblyUnbound(dunder_getitem_outcome) => { - Some(UnionType::from_two_elements( - db, - *dunder_next_return, - dunder_getitem_outcome.return_type(db), - )) - } - CallDunderError::CallError(CallErrorKind::NotCallable, _) => { - Some(*dunder_next_return) - } - CallDunderError::CallError(_, dunder_getitem_bindings) => { - let dunder_getitem_return = dunder_getitem_bindings.return_type(db); - Some(UnionType::from_two_elements( - db, - *dunder_next_return, - dunder_getitem_return, - )) - } - }, - - Self::UnboundIterAndGetitemError { - dunder_getitem_error, - } => dunder_getitem_error.return_type(db), - - Self::UnboundAiterError => None, - } - } - - /// Does this error concern a synchronous or asynchronous iterable? - fn mode(&self) -> EvaluationMode { - match self { - Self::IterCallError { mode, .. } => *mode, - Self::IterReturnsInvalidIterator { mode, .. } => *mode, - Self::PossiblyUnboundIterAndGetitemError { .. } - | Self::UnboundIterAndGetitemError { .. } => EvaluationMode::Sync, - Self::UnboundAiterError => EvaluationMode::Async, - } - } - - /// Reports the diagnostic for this error. - fn report_diagnostic( - &self, - context: &InferContext<'db, '_>, - iterable_type: Type<'db>, - iterable_node: ast::AnyNodeRef, - ) { - /// A little helper type for emitting a diagnostic - /// based on the variant of iteration error. - struct Reporter<'a> { - db: &'a dyn Db, - builder: LintDiagnosticGuardBuilder<'a, 'a>, - iterable_type: Type<'a>, - mode: EvaluationMode, - } - - impl<'a> Reporter<'a> { - /// Emit a diagnostic that is certain that `iterable_type` is not iterable. - /// - /// `because` should explain why `iterable_type` is not iterable. - #[expect(clippy::wrong_self_convention)] - fn is_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { - let mut diag = self.builder.into_diagnostic(format_args!( - "Object of type `{iterable_type}` is not {maybe_async}iterable", - iterable_type = self.iterable_type.display(self.db), - maybe_async = if self.mode.is_async() { "async-" } else { "" } - )); - diag.info(because); - diag - } - - /// Emit a diagnostic that is uncertain that `iterable_type` is not iterable. - /// - /// `because` should explain why `iterable_type` is likely not iterable. - fn may_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { - let mut diag = self.builder.into_diagnostic(format_args!( - "Object of type `{iterable_type}` may not be {maybe_async}iterable", - iterable_type = self.iterable_type.display(self.db), - maybe_async = if self.mode.is_async() { "async-" } else { "" } - )); - diag.info(because); - diag - } - } - - let Some(builder) = context.report_lint(&NOT_ITERABLE, iterable_node) else { - return; - }; - let db = context.db(); - let mode = self.mode(); - let reporter = Reporter { - db, - builder, - iterable_type, - mode, - }; - - // TODO: for all of these error variants, the "explanation" for the diagnostic - // (everything after the "because") should really be presented as a "help:", "note", - // or similar, rather than as part of the same sentence as the error message. - match self { - Self::IterCallError { - kind, - bindings, - mode, - } => { - let method = if mode.is_async() { - "__aiter__" - } else { - "__iter__" - }; - - match kind { - CallErrorKind::NotCallable => { - reporter.is_not(format_args!( - "Its `{method}` attribute has type `{dunder_iter_type}`, which is not callable", - dunder_iter_type = bindings.callable_type().display(db), - )); - } - CallErrorKind::PossiblyNotCallable => { - reporter.may_not(format_args!( - "Its `{method}` attribute (with type `{dunder_iter_type}`) \ - may not be callable", - dunder_iter_type = bindings.callable_type().display(db), - )); - } - CallErrorKind::BindingError => { - if bindings.is_single() { - reporter - .is_not(format_args!( - "Its `{method}` method has an invalid signature" - )) - .info(format_args!("Expected signature `def {method}(self): ...`")); - } else { - let mut diag = reporter.may_not(format_args!( - "Its `{method}` method may have an invalid signature" - )); - diag.info(format_args!( - "Type of `{method}` is `{dunder_iter_type}`", - dunder_iter_type = bindings.callable_type().display(db), - )); - diag.info(format_args!( - "Expected signature for `{method}` is `def {method}(self): ...`", - )); - } - } - } - } - - Self::IterReturnsInvalidIterator { - iterator, - dunder_error: dunder_next_error, - mode, - } => { - let dunder_iter_name = if mode.is_async() { - "__aiter__" - } else { - "__iter__" - }; - let dunder_next_name = if mode.is_async() { - "__anext__" - } else { - "__next__" - }; - match dunder_next_error { - CallDunderError::MethodNotAvailable => { - reporter.is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has no `{dunder_next_name}` method", - iterator_type = iterator.display(db), - )); - } - CallDunderError::PossiblyUnbound(_) => { - reporter.may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which may not have a `{dunder_next_name}` method", - iterator_type = iterator.display(db), - )); - } - CallDunderError::CallError(CallErrorKind::NotCallable, _) => { - reporter.is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has a `{dunder_next_name}` attribute that is not callable", - iterator_type = iterator.display(db), - )); - } - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => { - reporter.may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has a `{dunder_next_name}` attribute that may not be callable", - iterator_type = iterator.display(db), - )); - } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) - if bindings.is_single() => - { - reporter - .is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has an invalid `{dunder_next_name}` method", - iterator_type = iterator.display(db), - )) - .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); - } - CallDunderError::CallError(CallErrorKind::BindingError, _) => { - reporter - .may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which may have an invalid `{dunder_next_name}` method", - iterator_type = iterator.display(db), - )) - .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); - } - } - } - - Self::PossiblyUnboundIterAndGetitemError { - dunder_getitem_error, - .. - } => match dunder_getitem_error { - CallDunderError::MethodNotAvailable => { - reporter.may_not( - "It may not have an `__iter__` method \ - and it doesn't have a `__getitem__` method", - ); - } - CallDunderError::PossiblyUnbound(_) => { - reporter - .may_not("It may not have an `__iter__` method or a `__getitem__` method"); - } - CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => { - reporter.may_not(format_args!( - "It may not have an `__iter__` method \ - and its `__getitem__` attribute has type `{dunder_getitem_type}`, \ - which is not callable", - dunder_getitem_type = bindings.callable_type().display(db), - )); - } - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) - if bindings.is_single() => - { - reporter.may_not( - "It may not have an `__iter__` method \ - and its `__getitem__` attribute may not be callable", - ); - } - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { - reporter.may_not(format_args!( - "It may not have an `__iter__` method \ - and its `__getitem__` attribute (with type `{dunder_getitem_type}`) \ - may not be callable", - dunder_getitem_type = bindings.callable_type().display(db), - )); - } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) - if bindings.is_single() => - { - reporter - .may_not( - "It may not have an `__iter__` method \ - and its `__getitem__` method has an incorrect signature \ - for the old-style iteration protocol", - ) - .info( - "`__getitem__` must be at least as permissive as \ - `def __getitem__(self, key: int): ...` \ - to satisfy the old-style iteration protocol", - ); - } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) => { - reporter - .may_not(format_args!( - "It may not have an `__iter__` method \ - and its `__getitem__` method (with type `{dunder_getitem_type}`) \ - may have an incorrect signature for the old-style iteration protocol", - dunder_getitem_type = bindings.callable_type().display(db), - )) - .info( - "`__getitem__` must be at least as permissive as \ - `def __getitem__(self, key: int): ...` \ - to satisfy the old-style iteration protocol", - ); - } - }, - - Self::UnboundIterAndGetitemError { - dunder_getitem_error, - } => match dunder_getitem_error { - CallDunderError::MethodNotAvailable => { - reporter - .is_not("It doesn't have an `__iter__` method or a `__getitem__` method"); - } - CallDunderError::PossiblyUnbound(_) => { - reporter.is_not( - "It has no `__iter__` method and it may not have a `__getitem__` method", - ); - } - CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => { - reporter.is_not(format_args!( - "It has no `__iter__` method and \ - its `__getitem__` attribute has type `{dunder_getitem_type}`, \ - which is not callable", - dunder_getitem_type = bindings.callable_type().display(db), - )); - } - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) - if bindings.is_single() => - { - reporter.may_not( - "It has no `__iter__` method and its `__getitem__` attribute \ - may not be callable", - ); - } - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { - reporter.may_not( - "It has no `__iter__` method and its `__getitem__` attribute is invalid", - ).info(format_args!( - "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", - dunder_getitem_type = bindings.callable_type().display(db), - )); - } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) - if bindings.is_single() => - { - reporter - .is_not( - "It has no `__iter__` method and \ - its `__getitem__` method has an incorrect signature \ - for the old-style iteration protocol", - ) - .info( - "`__getitem__` must be at least as permissive as \ - `def __getitem__(self, key: int): ...` \ - to satisfy the old-style iteration protocol", - ); - } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) => { - reporter - .may_not(format_args!( - "It has no `__iter__` method and \ - its `__getitem__` method (with type `{dunder_getitem_type}`) \ - may have an incorrect signature for the old-style iteration protocol", - dunder_getitem_type = bindings.callable_type().display(db), - )) - .info( - "`__getitem__` must be at least as permissive as \ - `def __getitem__(self, key: int): ...` \ - to satisfy the old-style iteration protocol", - ); - } - }, - - IterationError::UnboundAiterError => { - reporter.is_not("It has no `__aiter__` method"); - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) enum BoolError<'db> { - /// The type has a `__bool__` attribute but it can't be called. - NotCallable { not_boolable_type: Type<'db> }, - - /// The type has a callable `__bool__` attribute, but it isn't callable - /// with the given arguments. - IncorrectArguments { - not_boolable_type: Type<'db>, - truthiness: Truthiness, - }, - - /// The type has a `__bool__` method, is callable with the given arguments, - /// but the return type isn't assignable to `bool`. - IncorrectReturnType { - not_boolable_type: Type<'db>, - return_type: Type<'db>, - }, - - /// A union type doesn't implement `__bool__` correctly. - Union { - union: UnionType<'db>, - truthiness: Truthiness, - }, - - /// Any other reason why the type can't be converted to a bool. - /// E.g. because calling `__bool__` returns in a union type and not all variants support `__bool__` or - /// because `__bool__` points to a type that has a possibly missing `__call__` method. - Other { not_boolable_type: Type<'db> }, -} - -impl<'db> BoolError<'db> { - pub(super) fn fallback_truthiness(&self) -> Truthiness { - match self { - BoolError::NotCallable { .. } - | BoolError::IncorrectReturnType { .. } - | BoolError::Other { .. } => Truthiness::Ambiguous, - BoolError::IncorrectArguments { truthiness, .. } - | BoolError::Union { truthiness, .. } => *truthiness, - } - } - - fn not_boolable_type(&self) -> Type<'db> { - match self { - BoolError::NotCallable { - not_boolable_type, .. - } - | BoolError::IncorrectArguments { - not_boolable_type, .. - } - | BoolError::Other { not_boolable_type } - | BoolError::IncorrectReturnType { - not_boolable_type, .. - } => *not_boolable_type, - BoolError::Union { union, .. } => Type::Union(*union), - } - } - - pub(super) fn report_diagnostic(&self, context: &InferContext, condition: impl Ranged) { - self.report_diagnostic_impl(context, condition.range()); - } - - fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) { - let Some(builder) = context.report_lint(&UNSUPPORTED_BOOL_CONVERSION, condition) else { - return; - }; - match self { - Self::IncorrectArguments { - not_boolable_type, .. - } => { - let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is not supported for type `{}`", - not_boolable_type.display(context.db()) - )); - let mut sub = SubDiagnostic::new( - SubDiagnosticSeverity::Info, - "`__bool__` methods must only have a `self` parameter", - ); - if let Some((func_span, parameter_span)) = not_boolable_type - .member(context.db(), "__bool__") - .into_lookup_result(context.db()) - .ok() - .and_then(|quals| quals.inner_type().parameter_span(context.db(), None)) - { - sub.annotate( - Annotation::primary(parameter_span).message("Incorrect parameters"), - ); - sub.annotate(Annotation::secondary(func_span).message("Method defined here")); - } - diag.sub(sub); - } - Self::IncorrectReturnType { - not_boolable_type, - return_type, - } => { - let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is not supported for type `{not_boolable}`", - not_boolable = not_boolable_type.display(context.db()), - )); - let mut sub = SubDiagnostic::new( - SubDiagnosticSeverity::Info, - format_args!( - "`{return_type}` is not assignable to `bool`", - return_type = return_type.display(context.db()), - ), - ); - if let Some((func_span, return_type_span)) = not_boolable_type - .member(context.db(), "__bool__") - .into_lookup_result(context.db()) - .ok() - .and_then(|quals| quals.inner_type().function_spans(context.db())) - .and_then(|spans| Some((spans.name, spans.return_type?))) - { - sub.annotate( - Annotation::primary(return_type_span).message("Incorrect return type"), - ); - sub.annotate(Annotation::secondary(func_span).message("Method defined here")); - } - diag.sub(sub); - } - Self::NotCallable { not_boolable_type } => { - let mut diag = builder.into_diagnostic(format_args!( - "Boolean conversion is not supported for type `{}`", - not_boolable_type.display(context.db()) - )); - let sub = SubDiagnostic::new( - SubDiagnosticSeverity::Info, - format_args!( - "`__bool__` on `{}` must be callable", - not_boolable_type.display(context.db()) - ), - ); - // TODO: It would be nice to create an annotation here for - // where `__bool__` is defined. At time of writing, I couldn't - // figure out a straight-forward way of doing this. ---AG - diag.sub(sub); - } - Self::Union { union, .. } => { - let first_error = union - .elements(context.db()) - .iter() - .find_map(|element| element.try_bool(context.db()).err()) - .unwrap(); - - builder.into_diagnostic(format_args!( - "Boolean conversion is not supported for union `{}` \ - because `{}` doesn't implement `__bool__` correctly", - Type::Union(*union).display(context.db()), - first_error.not_boolable_type().display(context.db()), - )); - } - - Self::Other { not_boolable_type } => { - builder.into_diagnostic(format_args!( - "Boolean conversion is not supported for type `{}`; \ - it incorrectly implements `__bool__`", - not_boolable_type.display(context.db()) - )); - } - } - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, get_size2::GetSize)] pub enum Truthiness { /// For an object `x`, `bool(x)` will always return `True` @@ -11391,6 +9870,26 @@ pub(super) fn determine_upper_bound<'db>( Type::instance(db, upper_bound) } +#[derive(Clone, Copy, Debug, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) enum EvaluationMode { + Sync, + Async, +} + +impl EvaluationMode { + pub(crate) const fn from_is_async(is_async: bool) -> Self { + if is_async { + EvaluationMode::Async + } else { + EvaluationMode::Sync + } + } + + pub(crate) const fn is_async(self) -> bool { + matches!(self, EvaluationMode::Async) + } +} + // Make sure that the `Type` enum does not grow unexpectedly. #[cfg(not(debug_assertions))] #[cfg(target_pointer_width = "64")] diff --git a/crates/ty_python_semantic/src/types/bool.rs b/crates/ty_python_semantic/src/types/bool.rs new file mode 100644 index 0000000000000..da5291e781a5a --- /dev/null +++ b/crates/ty_python_semantic/src/types/bool.rs @@ -0,0 +1,487 @@ +use ruff_db::diagnostic::{Annotation, SubDiagnostic, SubDiagnosticSeverity}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::{ + Db, + types::{ + CallArguments, CallDunderError, ClassType, CycleDetector, KnownClass, KnownInstanceType, + LiteralValueTypeKind, SubclassOfInner, Truthiness, Type, TypeContext, + TypeVarBoundOrConstraints, UnionType, call::CallErrorKind, + constraints::ConstraintSetBuilder, context::InferContext, + diagnostic::UNSUPPORTED_BOOL_CONVERSION, typed_dict::TypedDictField, + }, +}; + +impl<'db> Type<'db> { + /// Resolves the boolean value of the type and falls back to [`Truthiness::Ambiguous`] if the type doesn't implement `__bool__` correctly. + /// + /// This method should only be used outside type checking or when evaluating if a type + /// is truthy or falsy in a context where Python doesn't make an implicit `bool` call. + /// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls. + pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness { + self.try_bool_impl(db, true, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) + .unwrap_or_else(|err| err.fallback_truthiness()) + } + + /// Resolves the boolean value of a type. + /// + /// This is used to determine the value that would be returned + /// when `bool(x)` is called on an object `x`. + /// + /// Returns an error if the type doesn't implement `__bool__` correctly. + pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result> { + self.try_bool_impl(db, false, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) + } + + /// Resolves the boolean value of a type. + /// + /// Setting `allow_short_circuit` to `true` allows the implementation to + /// early return if the bool value of any union variant is `Truthiness::Ambiguous`. + /// Early returning shows a 1-2% perf improvement on our benchmarks because + /// `bool` (which doesn't care about errors) is used heavily when evaluating statically known branches. + /// + /// An alternative to this flag is to implement a trait similar to Rust's `Try` trait. + /// The advantage of that is that it would allow collecting the errors as well. However, + /// it is significantly more complex and duplicating the logic into `bool` without the error + /// handling didn't show any significant performance difference to when using the `allow_short_circuit` flag. + #[inline] + fn try_bool_impl( + &self, + db: &'db dyn Db, + allow_short_circuit: bool, + visitor: &TryBoolVisitor<'db>, + ) -> Result> { + let type_to_truthiness = |ty: Type<'db>| { + match ty.as_literal_value_kind() { + Some(LiteralValueTypeKind::Bool(bool_val)) => Truthiness::from(bool_val), + Some(LiteralValueTypeKind::Int(int_val)) => Truthiness::from(int_val.as_i64() != 0), + // anything else is handled lower down + _ => Truthiness::Ambiguous, + } + }; + + let try_dunders = || { + match self.try_call_dunder( + db, + "__bool__", + CallArguments::none(), + TypeContext::default(), + ) { + Ok(outcome) => { + let return_type = outcome.return_type(db); + if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + // The type has a `__bool__` method, but it doesn't return a + // boolean. + return Err(BoolError::IncorrectReturnType { + return_type, + not_boolable_type: *self, + }); + } + Ok(type_to_truthiness(return_type)) + } + + Err(CallDunderError::PossiblyUnbound(outcome)) => { + let return_type = outcome.return_type(db); + if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + // The type has a `__bool__` method, but it doesn't return a + // boolean. + return Err(BoolError::IncorrectReturnType { + return_type: outcome.return_type(db), + not_boolable_type: *self, + }); + } + + // Don't trust possibly missing `__bool__` method. + Ok(Truthiness::Ambiguous) + } + + Err(CallDunderError::MethodNotAvailable) => { + // We only consider `__len__` for tuples and `@final` types, + // since `__bool__` takes precedence + // and a subclass could add a `__bool__` method. + // + // TODO: with regards to tuple types, we intend to emit a diagnostic + // if a tuple subclass defines a `__bool__` method with a return type + // that is inconsistent with the tuple's length. Otherwise, the special + // handling for tuples here isn't sound. + if let Some(instance) = self.as_nominal_instance() { + if let Some(tuple_spec) = instance.tuple_spec(db) { + Ok(tuple_spec.truthiness()) + } else if instance.class(db).is_final(db) { + match self.try_call_dunder( + db, + "__len__", + CallArguments::none(), + TypeContext::default(), + ) { + Ok(outcome) => { + let return_type = outcome.return_type(db); + if return_type.is_assignable_to( + db, + KnownClass::SupportsIndex.to_instance(db), + ) { + Ok(type_to_truthiness(return_type)) + } else { + // TODO: should report a diagnostic similar to if return type of `__bool__` + // is not assignable to `bool` + Ok(Truthiness::Ambiguous) + } + } + // if a `@final` type does not define `__bool__` or `__len__`, it is always truthy + Err(CallDunderError::MethodNotAvailable) => { + Ok(Truthiness::AlwaysTrue) + } + // TODO: errors during a `__len__` call (if `__len__` exists) should be reported + // as diagnostics similar to errors during a `__bool__` call (when `__bool__` exists) + Err(_) => Ok(Truthiness::Ambiguous), + } + } else { + Ok(Truthiness::Ambiguous) + } + } else { + Ok(Truthiness::Ambiguous) + } + } + + Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => { + Err(BoolError::IncorrectArguments { + truthiness: type_to_truthiness(bindings.return_type(db)), + not_boolable_type: *self, + }) + } + + Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => { + Err(BoolError::NotCallable { + not_boolable_type: *self, + }) + } + + Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => { + Err(BoolError::Other { + not_boolable_type: *self, + }) + } + } + }; + + let try_union = |union: UnionType<'db>| { + let mut truthiness = None; + let mut all_not_callable = true; + let mut has_errors = false; + + for element in union.elements(db) { + let element_truthiness = + match element.try_bool_impl(db, allow_short_circuit, visitor) { + Ok(truthiness) => truthiness, + Err(err) => { + has_errors = true; + all_not_callable &= matches!(err, BoolError::NotCallable { .. }); + err.fallback_truthiness() + } + }; + + truthiness.get_or_insert(element_truthiness); + + if Some(element_truthiness) != truthiness { + truthiness = Some(Truthiness::Ambiguous); + + if allow_short_circuit { + return Ok(Truthiness::Ambiguous); + } + } + } + + if has_errors { + if all_not_callable { + return Err(BoolError::NotCallable { + not_boolable_type: *self, + }); + } + return Err(BoolError::Union { + union, + truthiness: truthiness.unwrap_or(Truthiness::Ambiguous), + }); + } + Ok(truthiness.unwrap_or(Truthiness::Ambiguous)) + }; + + let truthiness = match self { + Type::Dynamic(_) + | Type::Never + | Type::Callable(_) + | Type::TypeIs(_) + | Type::TypeGuard(_) => Truthiness::Ambiguous, + + Type::TypedDict(td) => { + if td.items(db).values().any(TypedDictField::is_required) { + Truthiness::AlwaysTrue + } else { + // We can potentially infer empty typeddicts as always falsy if they're `closed=True`, + // but as of 22-01-26 we don't yet support PEP 728. + Truthiness::Ambiguous + } + } + + Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => { + let constraints = ConstraintSetBuilder::new(); + let tracked_set = constraints.load(tracked_set.constraints(db)); + Truthiness::from(tracked_set.is_always_satisfied(db)) + } + + Type::FunctionLiteral(_) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::KnownBoundMethod(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::PropertyInstance(_) + | Type::BoundSuper(_) + | Type::KnownInstance(_) + | Type::SpecialForm(_) + | Type::AlwaysTruthy => Truthiness::AlwaysTrue, + + Type::AlwaysFalsy => Truthiness::AlwaysFalse, + + Type::ClassLiteral(class) => { + class + .metaclass_instance_type(db) + .try_bool_impl(db, allow_short_circuit, visitor)? + } + Type::GenericAlias(alias) => ClassType::from(*alias) + .metaclass_instance_type(db) + .try_bool_impl(db, allow_short_circuit, visitor)?, + + Type::SubclassOf(subclass_of_ty) => { + match subclass_of_ty.subclass_of().with_transposed_type_var(db) { + SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, + SubclassOfInner::Class(class) => { + Type::from(class).try_bool_impl(db, allow_short_circuit, visitor)? + } + SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar) + .try_bool_impl(db, allow_short_circuit, visitor)?, + } + } + + Type::TypeVar(bound_typevar) => { + match bound_typevar.typevar(db).bound_or_constraints(db) { + None => Truthiness::Ambiguous, + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.try_bool_impl(db, allow_short_circuit, visitor)? + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .as_type(db) + .try_bool_impl(db, allow_short_circuit, visitor)?, + } + } + + Type::NominalInstance(instance) => instance + .known_class(db) + .and_then(KnownClass::bool) + .map(Ok) + .unwrap_or_else(try_dunders)?, + + Type::ProtocolInstance(_) => try_dunders()?, + + Type::Union(union) => try_union(*union)?, + + Type::Intersection(_) => { + // TODO + Truthiness::Ambiguous + } + + Type::LiteralValue(literal) => match literal.kind() { + LiteralValueTypeKind::LiteralString => Truthiness::Ambiguous, + LiteralValueTypeKind::Enum(enum_type) => enum_type + .enum_class_instance(db) + .try_bool_impl(db, allow_short_circuit, visitor)?, + + LiteralValueTypeKind::Int(num) => Truthiness::from(num.as_i64() != 0), + LiteralValueTypeKind::Bool(bool) => Truthiness::from(bool), + LiteralValueTypeKind::String(str) => Truthiness::from(!str.value(db).is_empty()), + LiteralValueTypeKind::Bytes(bytes) => Truthiness::from(!bytes.value(db).is_empty()), + }, + + Type::TypeAlias(alias) => visitor.visit(*self, || { + alias + .value_type(db) + .try_bool_impl(db, allow_short_circuit, visitor) + })?, + Type::NewTypeInstance(newtype) => { + newtype + .concrete_base_type(db) + .try_bool_impl(db, allow_short_circuit, visitor)? + } + }; + + Ok(truthiness) + } +} + +/// A [`CycleDetector`] that is used in `try_bool` methods. +pub(crate) type TryBoolVisitor<'db> = + CycleDetector, Result>>; +pub(crate) struct TryBool; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum BoolError<'db> { + /// The type has a `__bool__` attribute but it can't be called. + NotCallable { not_boolable_type: Type<'db> }, + + /// The type has a callable `__bool__` attribute, but it isn't callable + /// with the given arguments. + IncorrectArguments { + not_boolable_type: Type<'db>, + truthiness: Truthiness, + }, + + /// The type has a `__bool__` method, is callable with the given arguments, + /// but the return type isn't assignable to `bool`. + IncorrectReturnType { + not_boolable_type: Type<'db>, + return_type: Type<'db>, + }, + + /// A union type doesn't implement `__bool__` correctly. + Union { + union: UnionType<'db>, + truthiness: Truthiness, + }, + + /// Any other reason why the type can't be converted to a bool. + /// E.g. because calling `__bool__` returns in a union type and not all variants support `__bool__` or + /// because `__bool__` points to a type that has a possibly missing `__call__` method. + Other { not_boolable_type: Type<'db> }, +} + +impl<'db> BoolError<'db> { + pub(super) fn fallback_truthiness(&self) -> Truthiness { + match self { + BoolError::NotCallable { .. } + | BoolError::IncorrectReturnType { .. } + | BoolError::Other { .. } => Truthiness::Ambiguous, + BoolError::IncorrectArguments { truthiness, .. } + | BoolError::Union { truthiness, .. } => *truthiness, + } + } + + fn not_boolable_type(&self) -> Type<'db> { + match self { + BoolError::NotCallable { + not_boolable_type, .. + } + | BoolError::IncorrectArguments { + not_boolable_type, .. + } + | BoolError::Other { not_boolable_type } + | BoolError::IncorrectReturnType { + not_boolable_type, .. + } => *not_boolable_type, + BoolError::Union { union, .. } => Type::Union(*union), + } + } + + pub(super) fn report_diagnostic(&self, context: &InferContext, condition: impl Ranged) { + self.report_diagnostic_impl(context, condition.range()); + } + + fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) { + let Some(builder) = context.report_lint(&UNSUPPORTED_BOOL_CONVERSION, condition) else { + return; + }; + match self { + Self::IncorrectArguments { + not_boolable_type, .. + } => { + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is not supported for type `{}`", + not_boolable_type.display(context.db()) + )); + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + "`__bool__` methods must only have a `self` parameter", + ); + if let Some((func_span, parameter_span)) = not_boolable_type + .member(context.db(), "__bool__") + .into_lookup_result(context.db()) + .ok() + .and_then(|quals| quals.inner_type().parameter_span(context.db(), None)) + { + sub.annotate( + Annotation::primary(parameter_span).message("Incorrect parameters"), + ); + sub.annotate(Annotation::secondary(func_span).message("Method defined here")); + } + diag.sub(sub); + } + Self::IncorrectReturnType { + not_boolable_type, + return_type, + } => { + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is not supported for type `{not_boolable}`", + not_boolable = not_boolable_type.display(context.db()), + )); + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "`{return_type}` is not assignable to `bool`", + return_type = return_type.display(context.db()), + ), + ); + if let Some((func_span, return_type_span)) = not_boolable_type + .member(context.db(), "__bool__") + .into_lookup_result(context.db()) + .ok() + .and_then(|quals| quals.inner_type().function_spans(context.db())) + .and_then(|spans| Some((spans.name, spans.return_type?))) + { + sub.annotate( + Annotation::primary(return_type_span).message("Incorrect return type"), + ); + sub.annotate(Annotation::secondary(func_span).message("Method defined here")); + } + diag.sub(sub); + } + Self::NotCallable { not_boolable_type } => { + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is not supported for type `{}`", + not_boolable_type.display(context.db()) + )); + let sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "`__bool__` on `{}` must be callable", + not_boolable_type.display(context.db()) + ), + ); + // TODO: It would be nice to create an annotation here for + // where `__bool__` is defined. At time of writing, I couldn't + // figure out a straight-forward way of doing this. ---AG + diag.sub(sub); + } + Self::Union { union, .. } => { + let first_error = union + .elements(context.db()) + .iter() + .find_map(|element| element.try_bool(context.db()).err()) + .unwrap(); + + builder.into_diagnostic(format_args!( + "Boolean conversion is not supported for union `{}` \ + because `{}` doesn't implement `__bool__` correctly", + Type::Union(*union).display(context.db()), + first_error.not_boolable_type().display(context.db()), + )); + } + + Self::Other { not_boolable_type } => { + builder.into_diagnostic(format_args!( + "Boolean conversion is not supported for type `{}`; \ + it incorrectly implements `__bool__`", + not_boolable_type.display(context.db()) + )); + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 152847048c90d..053ccbb40c39b 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -45,13 +45,12 @@ use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Paramete use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::{ BoundMethodType, BoundTypeVarIdentity, BoundTypeVarInstance, CallableSignature, CallableType, - CallableTypeKind, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, GenericAlias, - InternedConstraintSet, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, - LiteralValueTypeKind, MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, - SpecialFormType, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, TypeVarVariance, - UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, + CallableTypeKind, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, + EvaluationMode, GenericAlias, InternedConstraintSet, IntersectionType, KnownBoundMethodType, + KnownClass, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, NominalInstanceType, + PropertyInstanceType, SpecialFormType, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, + TypeVarVariance, UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, }; -use crate::unpack::EvaluationMode; use crate::{DisplaySettings, Program}; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion}; diff --git a/crates/ty_python_semantic/src/types/context_manager.rs b/crates/ty_python_semantic/src/types/context_manager.rs new file mode 100644 index 0000000000000..5ae29c83b48ef --- /dev/null +++ b/crates/ty_python_semantic/src/types/context_manager.rs @@ -0,0 +1,257 @@ +use crate::{ + Db, + types::{ + CallArguments, CallDunderError, EvaluationMode, Type, TypeContext, call::CallErrorKind, + context::InferContext, diagnostic::INVALID_CONTEXT_MANAGER, + }, +}; +use ruff_python_ast as ast; + +impl<'db> Type<'db> { + /// Returns the type bound from a context manager with type `self`. + /// + /// This method should only be used outside of type checking because it omits any errors. + /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. + pub(super) fn enter(self, db: &'db dyn Db) -> Type<'db> { + self.try_enter_with_mode(db, EvaluationMode::Sync) + .unwrap_or_else(|err| err.fallback_enter_type(db)) + } + + /// Returns the type bound from a context manager with type `self`. + /// + /// This method should only be used outside of type checking because it omits any errors. + /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. + pub(super) fn aenter(self, db: &'db dyn Db) -> Type<'db> { + self.try_enter_with_mode(db, EvaluationMode::Async) + .unwrap_or_else(|err| err.fallback_enter_type(db)) + } + + /// Given the type of an object that is used as a context manager (i.e. in a `with` statement), + /// return the return type of its `__enter__` or `__aenter__` method, which is bound to any potential targets. + /// + /// E.g., for the following `with` statement, given the type of `x`, infer the type of `y`: + /// ```python + /// with x as y: + /// pass + /// ``` + pub(super) fn try_enter_with_mode( + self, + db: &'db dyn Db, + mode: EvaluationMode, + ) -> Result, ContextManagerError<'db>> { + let (enter_method, exit_method) = match mode { + EvaluationMode::Async => ("__aenter__", "__aexit__"), + EvaluationMode::Sync => ("__enter__", "__exit__"), + }; + + let enter = self.try_call_dunder( + db, + enter_method, + CallArguments::none(), + TypeContext::default(), + ); + let exit = self.try_call_dunder( + db, + exit_method, + CallArguments::positional([Type::none(db), Type::none(db), Type::none(db)]), + TypeContext::default(), + ); + + // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). + match (enter, exit) { + (Ok(enter), Ok(_)) => { + let ty = enter.return_type(db); + Ok(if mode.is_async() { + ty.try_await(db).unwrap_or(Type::unknown()) + } else { + ty + }) + } + (Ok(enter), Err(exit_error)) => { + let ty = enter.return_type(db); + Err(ContextManagerError::Exit { + enter_return_type: if mode.is_async() { + ty.try_await(db).unwrap_or(Type::unknown()) + } else { + ty + }, + exit_error, + mode, + }) + } + // TODO: Use the `exit_ty` to determine if any raised exception is suppressed. + (Err(enter_error), Ok(_)) => Err(ContextManagerError::Enter(enter_error, mode)), + (Err(enter_error), Err(exit_error)) => Err(ContextManagerError::EnterAndExit { + enter_error, + exit_error, + mode, + }), + } + } +} + +/// Error returned if a type is not (or may not be) a context manager. +#[derive(Debug)] +pub(super) enum ContextManagerError<'db> { + Enter(CallDunderError<'db>, EvaluationMode), + Exit { + enter_return_type: Type<'db>, + exit_error: CallDunderError<'db>, + mode: EvaluationMode, + }, + EnterAndExit { + enter_error: CallDunderError<'db>, + exit_error: CallDunderError<'db>, + mode: EvaluationMode, + }, +} + +impl<'db> ContextManagerError<'db> { + pub(super) fn fallback_enter_type(&self, db: &'db dyn Db) -> Type<'db> { + self.enter_type(db).unwrap_or(Type::unknown()) + } + + /// Returns the `__enter__` or `__aenter__` return type if it is known, + /// or `None` if the type never has a callable `__enter__` or `__aenter__` attribute + fn enter_type(&self, db: &'db dyn Db) -> Option> { + match self { + Self::Exit { + enter_return_type, + exit_error: _, + mode: _, + } => Some(*enter_return_type), + Self::Enter(enter_error, _) + | Self::EnterAndExit { + enter_error, + exit_error: _, + mode: _, + } => match enter_error { + CallDunderError::PossiblyUnbound(call_outcome) => { + Some(call_outcome.return_type(db)) + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => None, + CallDunderError::CallError(_, bindings) => Some(bindings.return_type(db)), + CallDunderError::MethodNotAvailable => None, + }, + } + } + + pub(super) fn report_diagnostic( + &self, + context: &InferContext<'db, '_>, + context_expression_type: Type<'db>, + context_expression_node: ast::AnyNodeRef, + ) { + let Some(builder) = context.report_lint(&INVALID_CONTEXT_MANAGER, context_expression_node) + else { + return; + }; + + let mode = match self { + Self::Exit { mode, .. } | Self::Enter(_, mode) | Self::EnterAndExit { mode, .. } => { + *mode + } + }; + + let (enter_method, exit_method) = match mode { + EvaluationMode::Async => ("__aenter__", "__aexit__"), + EvaluationMode::Sync => ("__enter__", "__exit__"), + }; + + let format_call_dunder_error = |call_dunder_error: &CallDunderError<'db>, name: &str| { + match call_dunder_error { + CallDunderError::MethodNotAvailable => format!("it does not implement `{name}`"), + CallDunderError::PossiblyUnbound(_) => { + format!("the method `{name}` may be missing") + } + // TODO: Use more specific error messages for the different error cases. + // E.g. hint toward the union variant that doesn't correctly implement enter, + // distinguish between a not callable `__enter__` attribute and a wrong signature. + CallDunderError::CallError(_, _) => { + format!("it does not correctly implement `{name}`") + } + } + }; + + let format_call_dunder_errors = |error_a: &CallDunderError<'db>, + name_a: &str, + error_b: &CallDunderError<'db>, + name_b: &str| { + match (error_a, error_b) { + (CallDunderError::PossiblyUnbound(_), CallDunderError::PossiblyUnbound(_)) => { + format!("the methods `{name_a}` and `{name_b}` are possibly missing") + } + (CallDunderError::MethodNotAvailable, CallDunderError::MethodNotAvailable) => { + format!("it does not implement `{name_a}` and `{name_b}`") + } + (CallDunderError::CallError(_, _), CallDunderError::CallError(_, _)) => { + format!("it does not correctly implement `{name_a}` or `{name_b}`") + } + (_, _) => format!( + "{format_a}, and {format_b}", + format_a = format_call_dunder_error(error_a, name_a), + format_b = format_call_dunder_error(error_b, name_b) + ), + } + }; + + let db = context.db(); + + let formatted_errors = match self { + Self::Exit { + enter_return_type: _, + exit_error, + mode: _, + } => format_call_dunder_error(exit_error, exit_method), + Self::Enter(enter_error, _) => format_call_dunder_error(enter_error, enter_method), + Self::EnterAndExit { + enter_error, + exit_error, + mode: _, + } => format_call_dunder_errors(enter_error, enter_method, exit_error, exit_method), + }; + + // Suggest using `async with` if only async methods are available in a sync context, + // or suggest using `with` if only sync methods are available in an async context. + let with_kw = match mode { + EvaluationMode::Sync => "with", + EvaluationMode::Async => "async with", + }; + + let mut diag = builder.into_diagnostic(format_args!( + "Object of type `{}` cannot be used with `{}` because {}", + context_expression_type.display(db), + with_kw, + formatted_errors, + )); + + let (alt_mode, alt_enter_method, alt_exit_method, alt_with_kw) = match mode { + EvaluationMode::Sync => ("async", "__aenter__", "__aexit__", "async with"), + EvaluationMode::Async => ("sync", "__enter__", "__exit__", "with"), + }; + + let alt_enter = context_expression_type.try_call_dunder( + db, + alt_enter_method, + CallArguments::none(), + TypeContext::default(), + ); + let alt_exit = context_expression_type.try_call_dunder( + db, + alt_exit_method, + CallArguments::positional([Type::unknown(), Type::unknown(), Type::unknown()]), + TypeContext::default(), + ); + + if (alt_enter.is_ok() || matches!(alt_enter, Err(CallDunderError::CallError(..)))) + && (alt_exit.is_ok() || matches!(alt_exit, Err(CallDunderError::CallError(..)))) + { + diag.info(format_args!( + "Objects of type `{}` can be used as {} context managers", + context_expression_type.display(db), + alt_mode + )); + diag.info(format!("Consider using `{alt_with_kw}` here")); + } + } +} diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 243e3e8a1c9e2..0864c40f87d92 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -129,8 +129,8 @@ use crate::types::typed_dict::{ use crate::types::visitor::find_over_type; use crate::types::{ BoundTypeVarIdentity, CallDunderError, CallableBinding, CallableType, CallableTypeKind, - ClassType, DataclassParams, DynamicType, GenericAlias, InternedConstraintSet, InternedType, - IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, + ClassType, DataclassParams, DynamicType, EvaluationMode, GenericAlias, InternedConstraintSet, + InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, LiteralValueTypeKind, ManualPEP695TypeAliasType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, @@ -142,7 +142,7 @@ use crate::types::{ }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; -use crate::unpack::{EvaluationMode, UnpackPosition}; +use crate::unpack::UnpackPosition; use crate::{AnalysisSettings, Db, FxIndexSet, Program}; mod annotation_expression; diff --git a/crates/ty_python_semantic/src/types/iteration.rs b/crates/ty_python_semantic/src/types/iteration.rs new file mode 100644 index 0000000000000..3b4ad5d5441a7 --- /dev/null +++ b/crates/ty_python_semantic/src/types/iteration.rs @@ -0,0 +1,822 @@ +use crate::{ + Db, + types::{ + AwaitError, Bindings, CallArguments, CallDunderError, EvaluationMode, KnownClass, + LintDiagnosticGuard, LintDiagnosticGuardBuilder, LiteralValueTypeKind, Type, TypeContext, + TypeVarBoundOrConstraints, UnionType, + call::CallErrorKind, + context::InferContext, + diagnostic::NOT_ITERABLE, + todo_type, + tuple::{TupleSpec, TupleSpecBuilder}, + }, +}; +use ruff_python_ast as ast; +use std::borrow::Cow; + +impl<'db> Type<'db> { + /// Returns a tuple spec describing the elements that are produced when iterating over `self`. + /// + /// This method should only be used outside of type checking because it omits any errors. + /// For type checking, use [`try_iterate`](Self::try_iterate) instead. + pub(super) fn iterate(self, db: &'db dyn Db) -> Cow<'db, TupleSpec<'db>> { + self.try_iterate(db) + .unwrap_or_else(|err| Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db)))) + } + + /// Given the type of an object that is iterated over in some way, + /// return a tuple spec describing the type of objects that are yielded by that iteration. + /// + /// E.g., for the following call, given the type of `x`, infer the types of the values that are + /// splatted into `y`'s positional arguments: + /// ```python + /// y(*x) + /// ``` + pub(super) fn try_iterate( + self, + db: &'db dyn Db, + ) -> Result>, IterationError<'db>> { + self.try_iterate_with_mode(db, EvaluationMode::Sync) + } + + pub(super) fn try_iterate_with_mode( + self, + db: &'db dyn Db, + mode: EvaluationMode, + ) -> Result>, IterationError<'db>> { + fn non_async_special_case<'db>( + db: &'db dyn Db, + ty: Type<'db>, + ) -> Option>> { + // We will not infer precise heterogeneous tuple specs for literals with lengths above this threshold. + // The threshold here is somewhat arbitrary and conservative; it could be increased if needed. + // However, it's probably very rare to need heterogeneous unpacking inference for long string literals + // or bytes literals, and creating long heterogeneous tuple specs has a performance cost. + const MAX_TUPLE_LENGTH: usize = 128; + + match ty { + Type::NominalInstance(nominal) => nominal.tuple_spec(db), + Type::NewTypeInstance(newtype) => non_async_special_case(db, newtype.concrete_base_type(db)), + Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => { + Some(Cow::Owned(TupleSpec::homogeneous(todo_type!( + "*tuple[] annotations" + )))) + } + Type::LiteralValue(literal) => match literal.kind() { + LiteralValueTypeKind::Bytes(bytes) => { + let bytes_literal = bytes.value(db); + let spec = if bytes_literal.len() < MAX_TUPLE_LENGTH { + TupleSpec::heterogeneous( + bytes_literal + .iter() + .map(|b| Type::int_literal( i64::from(*b))), + ) + } else { + TupleSpec::homogeneous(KnownClass::Int.to_instance(db)) + }; + Some(Cow::Owned(spec)) + }, + LiteralValueTypeKind::String(string_literal_ty) => { + let string_literal = string_literal_ty.value(db); + let spec = if string_literal.len() < MAX_TUPLE_LENGTH { + TupleSpec::heterogeneous( + string_literal + .chars() + .map(|c| Type::string_literal(db, &c.to_string())), + ) + } else { + TupleSpec::homogeneous(Type::literal_string()) + }; + Some(Cow::Owned(spec)) + } + // N.B. This special case isn't strictly necessary, it's just an obvious optimization + LiteralValueTypeKind::LiteralString => { + Some(Cow::Owned(TupleSpec::homogeneous(ty))) + } + _ => None + } + Type::Never => { + // The dunder logic below would have us return `tuple[Never, ...]`, which eagerly + // simplifies to `tuple[()]`. That will will cause us to emit false positives if we + // index into the tuple. Using `tuple[Unknown, ...]` avoids these false positives. + // TODO: Consider removing this special case, and instead hide the indexing + // diagnostic in unreachable code. + Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))) + } + Type::TypeAlias(alias) => { + non_async_special_case(db, alias.value_type(db)) + } + Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db)? { + TypeVarBoundOrConstraints::UpperBound(bound) => { + non_async_special_case(db, bound) + } + TypeVarBoundOrConstraints::Constraints(constraints) => non_async_special_case(db, constraints.as_type(db)), + }, + Type::Union(union) => { + let elements = union.elements(db); + if elements.len() < MAX_TUPLE_LENGTH { + let mut elements_iter = elements.iter(); + let first_element_spec = elements_iter.next()?.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?; + let mut builder = TupleSpecBuilder::from(&*first_element_spec); + for element in elements_iter { + builder = builder.union(db, &*element.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?); + } + Some(Cow::Owned(builder.build())) + } else { + None + } + } + Type::Intersection(intersection) => { + // For intersections containing TypeVars with union bounds, we need to + // flatten the TypeVars first. This distributes the intersection over + // the union and simplifies, e.g.: + // `T & tuple[object, ...]` where `T: tuple[int, ...] | list[str]` + // becomes `(tuple[int, ...] & tuple[object, ...]) | (list[str] & tuple[object, ...])` + // which simplifies to `tuple[int, ...] | Never` = `tuple[int, ...]` + // + // After flattening, the result may be: + // - An intersection (if no union-bound typevars, or they didn't simplify). + // - A union of intersections (if distribution happened). + // - A simpler type (if it fully simplified). + // + // We then iterate over the flattened type. + let flattened = ty.flatten_typevars(db); + + // If flattening didn't change anything, iterate the intersection directly. + if flattened == ty { + let mut specs_iter = intersection.positive_elements_or_object(db).filter_map( + |element| element.try_iterate_with_mode(db, EvaluationMode::Sync).ok(), + ); + let first_spec = specs_iter.next()?; + let mut builder = TupleSpecBuilder::from(&*first_spec); + for spec in specs_iter { + // Two tuples cannot have incompatible specs unless the tuples themselves + // are disjoint. `IntersectionBuilder` eagerly simplifies such + // intersections to `Never`, so this should always return `Some`. + let Some(intersected) = builder.intersect(db, &spec) else { + return Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))); + }; + builder = intersected; + } + return Some(Cow::Owned(builder.build())); + } + + // Flattening changed the type; recursively iterate the flattened result. + non_async_special_case(db, flattened) + } + // N.B. This special case isn't strictly necessary, it's just an obvious optimization + Type::Dynamic(_) => Some(Cow::Owned(TupleSpec::homogeneous(ty))), + + Type::FunctionLiteral(_) + | Type::GenericAlias(_) + | Type::BoundMethod(_) + | Type::KnownBoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::Callable(_) + | Type::ModuleLiteral(_) + // We could infer a precise tuple spec for enum classes with members, + // but it's not clear whether that's worth the added complexity: + // you'd have to check that `EnumMeta.__iter__` is not overridden for it to be sound + // (enums can have `EnumMeta` subclasses as their metaclasses). + | Type::ClassLiteral(_) + | Type::SubclassOf(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::BoundSuper(_) + | Type::TypeIs(_) + | Type::TypeGuard(_) + | Type::TypedDict(_) => None + } + } + + if mode.is_async() { + let try_call_dunder_anext_on_iterator = |iterator: Type<'db>| -> Result< + Result, AwaitError<'db>>, + CallDunderError<'db>, + > { + iterator + .try_call_dunder( + db, + "__anext__", + CallArguments::none(), + TypeContext::default(), + ) + .map(|dunder_anext_outcome| dunder_anext_outcome.return_type(db).try_await(db)) + }; + + return match self.try_call_dunder( + db, + "__aiter__", + CallArguments::none(), + TypeContext::default(), + ) { + Ok(dunder_aiter_bindings) => { + let iterator = dunder_aiter_bindings.return_type(db); + match try_call_dunder_anext_on_iterator(iterator) { + Ok(Ok(result)) => Ok(Cow::Owned(TupleSpec::homogeneous(result))), + Ok(Err(AwaitError::InvalidReturnType(..))) => { + Err(IterationError::UnboundAiterError) + } // TODO: __anext__ is bound, but is not properly awaitable + Err(dunder_anext_error) | Ok(Err(AwaitError::Call(dunder_anext_error))) => { + Err(IterationError::IterReturnsInvalidIterator { + iterator, + dunder_error: dunder_anext_error, + mode, + }) + } + } + } + Err(CallDunderError::PossiblyUnbound(dunder_aiter_bindings)) => { + let iterator = dunder_aiter_bindings.return_type(db); + match try_call_dunder_anext_on_iterator(iterator) { + Ok(_) => Err(IterationError::IterCallError { + kind: CallErrorKind::PossiblyNotCallable, + bindings: dunder_aiter_bindings, + mode, + }), + Err(dunder_anext_error) => { + Err(IterationError::IterReturnsInvalidIterator { + iterator, + dunder_error: dunder_anext_error, + mode, + }) + } + } + } + Err(CallDunderError::CallError(kind, bindings)) => { + Err(IterationError::IterCallError { + kind, + bindings, + mode, + }) + } + Err(CallDunderError::MethodNotAvailable) => Err(IterationError::UnboundAiterError), + }; + } + + if let Some(special_case) = non_async_special_case(db, self) { + return Ok(special_case); + } + + let try_call_dunder_getitem = || { + self.try_call_dunder( + db, + "__getitem__", + CallArguments::positional([KnownClass::Int.to_instance(db)]), + TypeContext::default(), + ) + .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db)) + }; + + let try_call_dunder_next_on_iterator = |iterator: Type<'db>| { + iterator + .try_call_dunder( + db, + "__next__", + CallArguments::none(), + TypeContext::default(), + ) + .map(|dunder_next_outcome| dunder_next_outcome.return_type(db)) + }; + + let dunder_iter_result = self + .try_call_dunder( + db, + "__iter__", + CallArguments::none(), + TypeContext::default(), + ) + .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db)); + + match dunder_iter_result { + Ok(iterator) => { + // `__iter__` is definitely bound and calling it succeeds. + // See what calling `__next__` on the object returned by `__iter__` gives us... + try_call_dunder_next_on_iterator(iterator) + .map(|ty| Cow::Owned(TupleSpec::homogeneous(ty))) + .map_err( + |dunder_next_error| IterationError::IterReturnsInvalidIterator { + iterator, + dunder_error: dunder_next_error, + mode, + }, + ) + } + + // `__iter__` is possibly unbound... + Err(CallDunderError::PossiblyUnbound(dunder_iter_outcome)) => { + let iterator = dunder_iter_outcome.return_type(db); + + match try_call_dunder_next_on_iterator(iterator) { + Ok(dunder_next_return) => { + try_call_dunder_getitem() + .map(|dunder_getitem_return_type| { + // If `__iter__` is possibly unbound, + // but it returns an object that has a bound and valid `__next__` method, + // *and* the object has a bound and valid `__getitem__` method, + // we infer a union of the type returned by the `__next__` method + // and the type returned by the `__getitem__` method. + // + // No diagnostic is emitted; iteration will always succeed! + Cow::Owned(TupleSpec::homogeneous(UnionType::from_two_elements( + db, + dunder_next_return, + dunder_getitem_return_type, + ))) + }) + .map_err(|dunder_getitem_error| { + IterationError::PossiblyUnboundIterAndGetitemError { + dunder_next_return, + dunder_getitem_error, + } + }) + } + + Err(dunder_next_error) => Err(IterationError::IterReturnsInvalidIterator { + iterator, + dunder_error: dunder_next_error, + mode, + }), + } + } + + // `__iter__` is definitely bound but it can't be called with the expected arguments + Err(CallDunderError::CallError(kind, bindings)) => Err(IterationError::IterCallError { + kind, + bindings, + mode, + }), + + // There's no `__iter__` method. Try `__getitem__` instead... + Err(CallDunderError::MethodNotAvailable) => try_call_dunder_getitem() + .map(|ty| Cow::Owned(TupleSpec::homogeneous(ty))) + .map_err( + |dunder_getitem_error| IterationError::UnboundIterAndGetitemError { + dunder_getitem_error, + }, + ), + } + } +} + +/// Error returned if a type is not (or may not be) iterable. +#[derive(Debug)] +pub(super) enum IterationError<'db> { + /// The object being iterated over has a bound `__(a)iter__` method, + /// but calling it with the expected arguments results in an error. + IterCallError { + kind: CallErrorKind, + bindings: Box>, + mode: EvaluationMode, + }, + + /// The object being iterated over has a bound `__(a)iter__` method that can be called + /// with the expected types, but it returns an object that is not a valid iterator. + IterReturnsInvalidIterator { + /// The type of the object returned by the `__(a)iter__` method. + iterator: Type<'db>, + /// The error we encountered when we tried to call `__(a)next__` on the type + /// returned by `__(a)iter__` + dunder_error: CallDunderError<'db>, + /// Whether this is a synchronous or an asynchronous iterator. + mode: EvaluationMode, + }, + + /// The object being iterated over has a bound `__iter__` method that returns a + /// valid iterator. However, the `__iter__` method is possibly unbound, and there + /// either isn't a `__getitem__` method to fall back to, or calling the `__getitem__` + /// method returns some kind of error. + PossiblyUnboundIterAndGetitemError { + /// The type of the object returned by the `__next__` method on the iterator. + /// (The iterator being the type returned by the `__iter__` method on the iterable.) + dunder_next_return: Type<'db>, + /// The error we encountered when we tried to call `__getitem__` on the iterable. + dunder_getitem_error: CallDunderError<'db>, + }, + + /// The object being iterated over doesn't have an `__iter__` method. + /// It also either doesn't have a `__getitem__` method to fall back to, + /// or calling the `__getitem__` method returns some kind of error. + UnboundIterAndGetitemError { + dunder_getitem_error: CallDunderError<'db>, + }, + + /// The asynchronous iterable has no `__aiter__` method. + UnboundAiterError, +} + +impl<'db> IterationError<'db> { + pub(super) fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { + self.element_type(db).unwrap_or(Type::unknown()) + } + + /// Returns the element type if it is known, or `None` if the type is never iterable. + fn element_type(&self, db: &'db dyn Db) -> Option> { + let return_type = |result: Result, CallDunderError<'db>>| { + result + .map(|outcome| Some(outcome.return_type(db))) + .unwrap_or_else(|call_error| call_error.return_type(db)) + }; + + match self { + Self::IterReturnsInvalidIterator { + dunder_error, mode, .. + } => dunder_error.return_type(db).and_then(|ty| { + if mode.is_async() { + ty.try_await(db).ok() + } else { + Some(ty) + } + }), + + Self::IterCallError { + kind: _, + bindings: dunder_iter_bindings, + mode, + } => { + if mode.is_async() { + return_type(dunder_iter_bindings.return_type(db).try_call_dunder( + db, + "__anext__", + CallArguments::none(), + TypeContext::default(), + )) + .and_then(|ty| ty.try_await(db).ok()) + } else { + return_type(dunder_iter_bindings.return_type(db).try_call_dunder( + db, + "__next__", + CallArguments::none(), + TypeContext::default(), + )) + } + } + + Self::PossiblyUnboundIterAndGetitemError { + dunder_next_return, + dunder_getitem_error, + } => match dunder_getitem_error { + CallDunderError::MethodNotAvailable => Some(*dunder_next_return), + CallDunderError::PossiblyUnbound(dunder_getitem_outcome) => { + Some(UnionType::from_two_elements( + db, + *dunder_next_return, + dunder_getitem_outcome.return_type(db), + )) + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => { + Some(*dunder_next_return) + } + CallDunderError::CallError(_, dunder_getitem_bindings) => { + let dunder_getitem_return = dunder_getitem_bindings.return_type(db); + Some(UnionType::from_two_elements( + db, + *dunder_next_return, + dunder_getitem_return, + )) + } + }, + + Self::UnboundIterAndGetitemError { + dunder_getitem_error, + } => dunder_getitem_error.return_type(db), + + Self::UnboundAiterError => None, + } + } + + /// Does this error concern a synchronous or asynchronous iterable? + fn mode(&self) -> EvaluationMode { + match self { + Self::IterCallError { mode, .. } => *mode, + Self::IterReturnsInvalidIterator { mode, .. } => *mode, + Self::PossiblyUnboundIterAndGetitemError { .. } + | Self::UnboundIterAndGetitemError { .. } => EvaluationMode::Sync, + Self::UnboundAiterError => EvaluationMode::Async, + } + } + + /// Reports the diagnostic for this error. + pub(super) fn report_diagnostic( + &self, + context: &InferContext<'db, '_>, + iterable_type: Type<'db>, + iterable_node: ast::AnyNodeRef, + ) { + /// A little helper type for emitting a diagnostic + /// based on the variant of iteration error. + struct Reporter<'a> { + db: &'a dyn Db, + builder: LintDiagnosticGuardBuilder<'a, 'a>, + iterable_type: Type<'a>, + mode: EvaluationMode, + } + + impl<'a> Reporter<'a> { + /// Emit a diagnostic that is certain that `iterable_type` is not iterable. + /// + /// `because` should explain why `iterable_type` is not iterable. + #[expect(clippy::wrong_self_convention)] + fn is_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { + let mut diag = self.builder.into_diagnostic(format_args!( + "Object of type `{iterable_type}` is not {maybe_async}iterable", + iterable_type = self.iterable_type.display(self.db), + maybe_async = if self.mode.is_async() { "async-" } else { "" } + )); + diag.info(because); + diag + } + + /// Emit a diagnostic that is uncertain that `iterable_type` is not iterable. + /// + /// `because` should explain why `iterable_type` is likely not iterable. + fn may_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { + let mut diag = self.builder.into_diagnostic(format_args!( + "Object of type `{iterable_type}` may not be {maybe_async}iterable", + iterable_type = self.iterable_type.display(self.db), + maybe_async = if self.mode.is_async() { "async-" } else { "" } + )); + diag.info(because); + diag + } + } + + let Some(builder) = context.report_lint(&NOT_ITERABLE, iterable_node) else { + return; + }; + let db = context.db(); + let mode = self.mode(); + let reporter = Reporter { + db, + builder, + iterable_type, + mode, + }; + + // TODO: for all of these error variants, the "explanation" for the diagnostic + // (everything after the "because") should really be presented as a "help:", "note", + // or similar, rather than as part of the same sentence as the error message. + match self { + Self::IterCallError { + kind, + bindings, + mode, + } => { + let method = if mode.is_async() { + "__aiter__" + } else { + "__iter__" + }; + + match kind { + CallErrorKind::NotCallable => { + reporter.is_not(format_args!( + "Its `{method}` attribute has type `{dunder_iter_type}`, which is not callable", + dunder_iter_type = bindings.callable_type().display(db), + )); + } + CallErrorKind::PossiblyNotCallable => { + reporter.may_not(format_args!( + "Its `{method}` attribute (with type `{dunder_iter_type}`) \ + may not be callable", + dunder_iter_type = bindings.callable_type().display(db), + )); + } + CallErrorKind::BindingError => { + if bindings.is_single() { + reporter + .is_not(format_args!( + "Its `{method}` method has an invalid signature" + )) + .info(format_args!("Expected signature `def {method}(self): ...`")); + } else { + let mut diag = reporter.may_not(format_args!( + "Its `{method}` method may have an invalid signature" + )); + diag.info(format_args!( + "Type of `{method}` is `{dunder_iter_type}`", + dunder_iter_type = bindings.callable_type().display(db), + )); + diag.info(format_args!( + "Expected signature for `{method}` is `def {method}(self): ...`", + )); + } + } + } + } + + Self::IterReturnsInvalidIterator { + iterator, + dunder_error: dunder_next_error, + mode, + } => { + let dunder_iter_name = if mode.is_async() { + "__aiter__" + } else { + "__iter__" + }; + let dunder_next_name = if mode.is_async() { + "__anext__" + } else { + "__next__" + }; + match dunder_next_error { + CallDunderError::MethodNotAvailable => { + reporter.is_not(format_args!( + "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ + which has no `{dunder_next_name}` method", + iterator_type = iterator.display(db), + )); + } + CallDunderError::PossiblyUnbound(_) => { + reporter.may_not(format_args!( + "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ + which may not have a `{dunder_next_name}` method", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => { + reporter.is_not(format_args!( + "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ + which has a `{dunder_next_name}` attribute that is not callable", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => { + reporter.may_not(format_args!( + "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ + which has a `{dunder_next_name}` attribute that may not be callable", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .is_not(format_args!( + "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ + which has an invalid `{dunder_next_name}` method", + iterator_type = iterator.display(db), + )) + .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); + } + CallDunderError::CallError(CallErrorKind::BindingError, _) => { + reporter + .may_not(format_args!( + "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ + which may have an invalid `{dunder_next_name}` method", + iterator_type = iterator.display(db), + )) + .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); + } + } + } + + Self::PossiblyUnboundIterAndGetitemError { + dunder_getitem_error, + .. + } => match dunder_getitem_error { + CallDunderError::MethodNotAvailable => { + reporter.may_not( + "It may not have an `__iter__` method \ + and it doesn't have a `__getitem__` method", + ); + } + CallDunderError::PossiblyUnbound(_) => { + reporter + .may_not("It may not have an `__iter__` method or a `__getitem__` method"); + } + CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => { + reporter.may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute has type `{dunder_getitem_type}`, \ + which is not callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) + if bindings.is_single() => + { + reporter.may_not( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute may not be callable", + ); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { + reporter.may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute (with type `{dunder_getitem_type}`) \ + may not be callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .may_not( + "It may not have an `__iter__` method \ + and its `__getitem__` method has an incorrect signature \ + for the old-style iteration protocol", + ) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) => { + reporter + .may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` method (with type `{dunder_getitem_type}`) \ + may have an incorrect signature for the old-style iteration protocol", + dunder_getitem_type = bindings.callable_type().display(db), + )) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + }, + + Self::UnboundIterAndGetitemError { + dunder_getitem_error, + } => match dunder_getitem_error { + CallDunderError::MethodNotAvailable => { + reporter + .is_not("It doesn't have an `__iter__` method or a `__getitem__` method"); + } + CallDunderError::PossiblyUnbound(_) => { + reporter.is_not( + "It has no `__iter__` method and it may not have a `__getitem__` method", + ); + } + CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => { + reporter.is_not(format_args!( + "It has no `__iter__` method and \ + its `__getitem__` attribute has type `{dunder_getitem_type}`, \ + which is not callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) + if bindings.is_single() => + { + reporter.may_not( + "It has no `__iter__` method and its `__getitem__` attribute \ + may not be callable", + ); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { + reporter.may_not( + "It has no `__iter__` method and its `__getitem__` attribute is invalid", + ).info(format_args!( + "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .is_not( + "It has no `__iter__` method and \ + its `__getitem__` method has an incorrect signature \ + for the old-style iteration protocol", + ) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) => { + reporter + .may_not(format_args!( + "It has no `__iter__` method and \ + its `__getitem__` method (with type `{dunder_getitem_type}`) \ + may have an incorrect signature for the old-style iteration protocol", + dunder_getitem_type = bindings.callable_type().display(db), + )) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + }, + + IterationError::UnboundAiterError => { + reporter.is_not("It has no `__aiter__` method"); + } + } + } +} diff --git a/crates/ty_python_semantic/src/unpack.rs b/crates/ty_python_semantic/src/unpack.rs index cb07f2570a725..c9acc3fcfd95f 100644 --- a/crates/ty_python_semantic/src/unpack.rs +++ b/crates/ty_python_semantic/src/unpack.rs @@ -7,6 +7,7 @@ use crate::Db; use crate::ast_node_ref::AstNodeRef; use crate::semantic_index::expression::Expression; use crate::semantic_index::scope::{FileScopeId, ScopeId}; +use crate::types::EvaluationMode; /// This ingredient represents a single unpacking. /// @@ -102,26 +103,6 @@ impl<'db> UnpackValue<'db> { } } -#[derive(Clone, Copy, Debug, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) enum EvaluationMode { - Sync, - Async, -} - -impl EvaluationMode { - pub(crate) const fn from_is_async(is_async: bool) -> Self { - if is_async { - EvaluationMode::Async - } else { - EvaluationMode::Sync - } - } - - pub(crate) const fn is_async(self) -> bool { - matches!(self, EvaluationMode::Async) - } -} - #[derive(Clone, Copy, Debug, Hash, salsa::Update, get_size2::GetSize)] pub(crate) enum UnpackKind { /// An iterable expression like the one in a `for` loop or a comprehension. From f8b0b0fc1531cc92b78635f37f183cbdcb1cd537 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 2 Mar 2026 13:51:24 -0500 Subject: [PATCH 174/261] [ty] Hand-roll memoization caches for constraint sets (#23538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates how we manage the interning and memoization of our constraint set BDD implementation. Before we relied on Salsa interning and tracking. This had the nice property that everything was cached globally across the entire process, which in theory gives us space savings when the same constraint sets are created for different regions of source code. However, it made us very susceptible to changes in the ordering of salsa IDs. Salsa IDs are also assigned globally, in the order that interned structs or tracked method results are created. That means that if e.g. a particular constraint is created for two unrelated regions of code, there's no guarantee about how that constraint's salsa ID will compare to the IDs of the other constraints in the BDD. Since we were using constraint IDs to define our BDD variable ordering, this would sometimes give us different BDD structures for different (identical) invocations of ty. This was leading to a lot of nondeterminism in our CI jobs. We now rely on a new `ConstraintSetBuilder` type to define _local_ caching of BDD nodes and operations. The type was introduced as a no-op refactoring in https://github.com/astral-sh/ruff/pull/23600. This PR updates that type to actually take over responsiblity for the interning and memoization caches. This opens up some other potential optimizations, but I've kept this PR purposefully limited in scope — with one exception, we cache exactly the same things as before, just in a hand-roll `FxHashMap` instead of via a magic salsa macro. (That one exception is that we now intern typevars locally in the new builder, even though they are already salsa-interned globally. This is needed to give them a stable ID within the builder.) --------- Co-authored-by: Alex Waygood --- .../src/semantic_index/definition.rs | 5 - crates/ty_python_semantic/src/types.rs | 15 +- crates/ty_python_semantic/src/types/bool.rs | 2 +- .../ty_python_semantic/src/types/call/bind.rs | 15 +- .../src/types/constraints.rs | 2914 +++++++++++------ .../ty_python_semantic/src/types/generics.rs | 18 +- .../src/types/infer/builder.rs | 2 +- .../types/infer/builder/binary_expressions.rs | 8 +- .../src/types/infer/comparisons.rs | 4 +- 9 files changed, 1958 insertions(+), 1025 deletions(-) diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 99bacd9e4c1ba..26c570e6458c8 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -25,12 +25,7 @@ use crate::unpack::{Unpack, UnpackPosition}; /// because a new scope gets inserted before the `Definition` or a new place is inserted /// before this `Definition`. However, the ID can be considered stable and it is okay to use /// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs. -/// -/// # Ordering -/// Ordering is based on the definition's salsa-assigned id and not on its values. -/// The id may change between runs, or when the definition was garbage collected and recreated. #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(Ord, PartialOrd)] pub struct Definition<'db> { /// The file in which the definition occurs. pub file: File, diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0963ad40de8ca..eaaca8786181f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6937,12 +6937,7 @@ impl TypeVarKind { /// This represents the core identity of a typevar, independent of its bounds or constraints. Two /// typevars have the same identity if they represent the same logical typevar, even if their /// bounds have been materialized differently. -/// -/// # Ordering -/// Ordering is based on the identity's salsa-assigned id and not on its values. -/// The id may change between runs, or when the identity was garbage collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -#[derive(PartialOrd, Ord)] pub struct TypeVarIdentity<'db> { /// The name of this TypeVar (e.g. `T`) #[returns(ref)] @@ -7479,9 +7474,7 @@ fn lazy_default_cycle_recover<'db>( } /// Where a type variable is bound and usable. -#[derive( - Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Update, get_size2::GetSize, -)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] pub enum BindingContext<'db> { /// The definition of the generic class, function, or type alias that binds this typevar. Definition(Definition<'db>), @@ -7509,7 +7502,7 @@ impl<'db> BindingContext<'db> { } } -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, get_size2::GetSize)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, get_size2::GetSize)] pub enum ParamSpecAttrKind { Args, Kwargs, @@ -7530,9 +7523,7 @@ impl std::fmt::Display for ParamSpecAttrKind { /// independent of the typevar's bounds or constraints. Two bound typevars have the same identity /// if they represent the same logical typevar bound in the same context, even if their bounds /// have been materialized differently. -#[derive( - Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, get_size2::GetSize, salsa::Update, -)] +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] pub struct BoundTypeVarIdentity<'db> { pub(crate) identity: TypeVarIdentity<'db>, pub(crate) binding_context: BindingContext<'db>, diff --git a/crates/ty_python_semantic/src/types/bool.rs b/crates/ty_python_semantic/src/types/bool.rs index da5291e781a5a..954e2614e8bb2 100644 --- a/crates/ty_python_semantic/src/types/bool.rs +++ b/crates/ty_python_semantic/src/types/bool.rs @@ -224,7 +224,7 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => { let constraints = ConstraintSetBuilder::new(); - let tracked_set = constraints.load(tracked_set.constraints(db)); + let tracked_set = constraints.load(db, tracked_set.constraints(db)); Truthiness::from(tracked_set.is_always_satisfied(db)) } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 053ccbb40c39b..5014349f05fb0 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1806,7 +1806,7 @@ impl<'db> Bindings<'db> { ty_a.when_subtype_of_assuming( db, *ty_b, - constraints.load(tracked.constraints(db)), + constraints.load(db, tracked.constraints(db)), constraints, InferableTypeVars::None, ) @@ -1830,8 +1830,8 @@ impl<'db> Bindings<'db> { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let lhs = constraints.load(tracked.constraints(db)); - let rhs = constraints.load(other.constraints(db)); + let lhs = constraints.load(db, tracked.constraints(db)); + let rhs = constraints.load(db, other.constraints(db)); lhs.implies(db, constraints, || rhs) }); let tracked = InternedConstraintSet::new(db, result); @@ -1871,9 +1871,12 @@ impl<'db> Bindings<'db> { }; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(tracked.constraints(db)); - let result = - set.satisfied_by_all_typevars(db, InferableTypeVars::One(&inferable)); + let set = constraints.load(db, tracked.constraints(db)); + let result = set.satisfied_by_all_typevars( + db, + &constraints, + InferableTypeVars::One(&inferable), + ); overload.set_return_type(Type::bool_literal(result)); } diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index c2c15a6ad1943..b75d19c18b4d4 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -66,7 +66,7 @@ //! //! [bdd]: https://en.wikipedia.org/wiki/Binary_decision_diagram -use std::cell::RefCell; +use std::cell::{Ref, RefCell}; use std::cmp::Ordering; use std::fmt::{Debug, Display}; use std::marker::PhantomData; @@ -74,8 +74,8 @@ use std::ops::Range; use indexmap::map::Entry; use itertools::Itertools; +use ruff_index::{Idx, IndexVec, newtype_index}; use rustc_hash::{FxHashMap, FxHashSet}; -use salsa::plumbing::AsId; use smallvec::SmallVec; use crate::types::class::GenericAlias; @@ -87,7 +87,7 @@ use crate::types::{ BoundTypeVarIdentity, BoundTypeVarInstance, IntersectionType, Type, TypeVarBoundOrConstraints, UnionType, walk_bound_type_var_type, }; -use crate::{Db, FxIndexMap, FxIndexSet, FxOrderSet}; +use crate::{Db, FxIndexMap, FxIndexSet}; /// An extension trait for building constraint sets from [`Option`] values. pub(crate) trait OptionConstraintsExtension { @@ -173,8 +173,9 @@ where builder: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, ) -> ConstraintSet<'db, 'c> { - let node = Node::distributed_or( + let node = NodeId::distributed_or( db, + builder, self.map(|element| { let constraint = f(element); constraint.verify_builder(builder); @@ -190,8 +191,9 @@ where builder: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, ) -> ConstraintSet<'db, 'c> { - let node = Node::distributed_and( + let node = NodeId::distributed_and( db, + builder, self.map(|element| { let constraint = f(element); constraint.verify_builder(builder); @@ -202,11 +204,28 @@ where } } +/// An owned copy of a [`ConstraintSet`]. Unlike [`ConstraintSet`], this type owns the storage +/// arenas that hold its BDD. +/// +/// This type is never created as part of the core type inference algorithms; it is only used by +/// the [`InternedConstraintSet`][crate::types::InternedConstraintSet] type, which is the wrapper +/// type that lets us create and operate on constraint sets in our mdtests. That means we don't +/// have to be overly worried about the efficiency of this type. +/// +/// Note that you cannot interrogate an owned constraint set in any useful way. Instead, you must +/// [`load`][ConstraintSetBuilder::load] it into a new builder, and query the result. #[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] pub struct OwnedConstraintSet<'db> { /// The BDD representing this constraint set - node: Node<'db>, - storage: ConstraintSetStorage<'db>, + node: NodeId, + + /// The constraints arena for this BDD. This is extracted from the [`ConstraintSetBuilder`] + /// when an owned constraint set is constructed. + constraints: IndexVec>, + + /// The nodes arena for this BDD. This is extracted from the [`ConstraintSetBuilder`] when an + /// owned constraint set is constructed. + nodes: IndexVec, } /// A set of constraints under which a type property holds. @@ -222,13 +241,17 @@ pub struct OwnedConstraintSet<'db> { #[derive(Clone, Copy)] pub struct ConstraintSet<'db, 'c> { /// The BDD representing this constraint set - node: Node<'db>, + node: NodeId, + + /// A reference to the builder that holds the storage for this constraint set's BDD builder: &'c ConstraintSetBuilder<'db>, + + /// Ensures that the `'c` lifetime is invariant _invariant: PhantomData &'c ()>, } impl<'db, 'c> ConstraintSet<'db, 'c> { - fn from_node(builder: &'c ConstraintSetBuilder<'db>, node: Node<'db>) -> Self { + fn from_node(builder: &'c ConstraintSetBuilder<'db>, node: NodeId) -> Self { Self { node, builder, @@ -237,11 +260,11 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } fn never(builder: &'c ConstraintSetBuilder<'db>) -> Self { - Self::from_node(builder, Node::AlwaysFalse) + Self::from_node(builder, ALWAYS_FALSE) } fn always(builder: &'c ConstraintSetBuilder<'db>) -> Self { - Self::from_node(builder, Node::AlwaysTrue) + Self::from_node(builder, ALWAYS_TRUE) } pub(crate) fn from_bool(builder: &'c ConstraintSetBuilder<'db>, b: bool) -> Self { @@ -262,10 +285,11 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { ) -> Self { Self::from_node( builder, - ConstrainedTypeVar::new_node(db, typevar, lower, upper), + Constraint::new_node(db, builder, typevar, lower, upper), ) } + /// Verifies that this constraint set was created by `builder` #[track_caller] fn verify_builder(self, builder: &'c ConstraintSetBuilder<'db>) { debug_assert!(std::ptr::eq(self.builder, builder)); @@ -273,12 +297,12 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns whether this constraint set never holds pub(crate) fn is_never_satisfied(self, db: &'db dyn Db) -> bool { - self.node.is_never_satisfied(db) + self.node.is_never_satisfied(db, self.builder) } /// Returns whether this constraint set always holds pub(crate) fn is_always_satisfied(self, db: &'db dyn Db) -> bool { - self.node.is_always_satisfied(db) + self.node.is_always_satisfied(db, self.builder) } /// Returns whether this constraint set contains any cycles between typevars. If it does, then @@ -363,15 +387,17 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { BoundTypeVarIdentity<'db>, FxHashSet>, > = FxHashMap::default(); - self.node.for_each_constraint(db, &mut |constraint, _| { - let visitor = CollectReachability::default(); - visitor.visit_type(db, constraint.lower(db)); - visitor.visit_type(db, constraint.upper(db)); - reachable_typevars - .entry(constraint.typevar(db).identity(db)) - .or_default() - .extend(visitor.reachable_typevars.into_inner()); - }); + self.node + .for_each_constraint(self.builder, &mut |constraint, _| { + let visitor = CollectReachability::default(); + let constraint = self.builder.constraint_data(constraint); + visitor.visit_type(db, constraint.lower); + visitor.visit_type(db, constraint.upper); + reachable_typevars + .entry(constraint.typevar.identity(db)) + .or_default() + .extend(visitor.reachable_typevars.into_inner()); + }); // Then perform a depth-first search to see if there are any cycles. let mut discovered: FxHashSet> = FxHashSet::default(); @@ -399,7 +425,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { rhs: Type<'db>, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.implies_subtype_of(db, lhs, rhs)) + Self::from_node(builder, self.node.implies_subtype_of(db, builder, lhs, rhs)) } /// Returns whether this constraint set is satisfied by all of the typevars that it mentions. @@ -419,9 +445,11 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn satisfied_by_all_typevars( &self, db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, ) -> bool { - self.node.satisfied_by_all_typevars(db, inferable) + self.verify_builder(builder); + self.node.satisfied_by_all_typevars(db, builder, inferable) } /// Updates this constraint set to hold the union of itself and another constraint set. @@ -430,12 +458,12 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// nodes. pub(crate) fn union( &mut self, - db: &'db dyn Db, + _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, other: Self, ) -> Self { self.verify_builder(builder); - self.node = self.node.or_with_offset(db, other.node); + self.node = self.node.or_with_offset(builder, other.node); *self } @@ -445,19 +473,19 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// nodes. pub(crate) fn intersect( &mut self, - db: &'db dyn Db, + _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, other: Self, ) -> Self { self.verify_builder(builder); - self.node = self.node.and_with_offset(db, other.node); + self.node = self.node.and_with_offset(builder, other.node); *self } /// Returns the negation of this constraint set. - pub(crate) fn negate(self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>) -> Self { + pub(crate) fn negate(self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.negate(db)) + Self::from_node(builder, self.node.negate(builder)) } /// Returns the intersection of this constraint set and another. The other constraint set is @@ -521,12 +549,12 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// nodes. pub(crate) fn iff( self, - db: &'db dyn Db, + _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, other: Self, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.iff_with_offset(db, other.node)) + Self::from_node(builder, self.node.iff_with_offset(builder, other.node)) } /// Reduces the set of inferable typevars for this constraint set. You provide an iterator of @@ -541,27 +569,43 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { to_remove: impl IntoIterator>, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.exists(db, to_remove)) + Self::from_node(builder, self.node.exists(db, builder, to_remove)) } - pub(crate) fn solutions(self, db: &'db dyn Db) -> Solutions<'db> { + pub(crate) fn solutions( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + ) -> Solutions<'db, 'c> { + self.verify_builder(builder); + // If the constraint set is cyclic, we'll hit an infinite expansion when trying to add type // mappings for it. if self.is_cyclic(db) { return Solutions::Unsatisfiable; } - self.node.solutions(db) + self.node.solutions(db, builder) } #[expect(dead_code)] // Keep this around for debugging purposes pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { - self.node.simplify_for_display(db).display(db) + self.node + .simplify_for_display(db, self.builder) + .display(db, self.builder) } #[expect(dead_code)] // Keep this around for debugging purposes - pub(crate) fn display_graph(self, db: &'db dyn Db, prefix: &dyn Display) -> impl Display { - self.node.display_graph(db, prefix) + pub(crate) fn display_graph<'a>( + self, + db: &'db dyn Db, + prefix: &'a dyn Display, + ) -> impl Display + 'a + where + 'db: 'a, + 'c: 'a, + { + self.node.display_graph(db, self.builder, prefix) } } @@ -573,14 +617,68 @@ impl Debug for ConstraintSet<'_, '_> { } } +/// Holds the storage for the BDD structure of a related collection of constraint sets. +/// +/// This is usually passed around by shared reference to avoid convoluted APIs that thread mutable +/// references to the builder back and forth. +/// +/// All of our BDD algorithms rely heavily on interning and memoization, for both correctness and +/// efficiency. These caches are only unique within the context of a particular builder. We do not +/// cache globally across the entire ty process. (The main reason is to avoid any dependencies on +/// the particular order in which files or expressions are visited during type checking. A minor +/// additional benefit is that the builder does not need to be thread-safe or impl [`Sync`].) +/// +/// Most core type inference algorithms create a builder, create one or more constraint sets in the +/// builder, interrogate those constraint sets, and then throw the builder away. +/// +/// TODO: We are considering creating a single builder in `TypeInferenceBuilder` that would be +/// shared across an entire inference region. That would give us even more sharing opportunities, +/// which could be highly impactful, since it's likely that there will be types and constraints +/// that are repeated within a region. It should still give us the stability that we need, because +/// once we determine that we need _something_ from an inference regions, we always infer _all_ of +/// the definitions and expressions in that region, in a stable order. #[derive(Default)] pub(crate) struct ConstraintSetBuilder<'db> { storage: RefCell>, } -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] +#[derive(Clone, Debug, Default, Eq, PartialEq, get_size2::GetSize)] struct ConstraintSetStorage<'db> { - _dummy: PhantomData<&'db ()>, + /// Constraints are the variables of our BDD. They are interned to give them a space-efficient + /// identity. Constraints are added to this arena as they are encountered when constructing + /// constraint sets. The ordering within the arena defines the BDD variable ordering in our BDD + /// structures. + constraints: IndexVec>, + + /// Typevars are interned so that they have a stable ordering within this builder, which does + /// not depend on their salsa IDs. (The salsa IDs are not stable, since each typevar can be + /// used (possibly indirectly) in expressions in different files, and there are no guarantees + /// about the order or the speed that we process each file.) + /// + /// The ordering of typevars within this arena defines which typevars can be the lower/upper + /// bounds of another (e.g., whether we encode `T ≤ U` as `Never ≤ T ≤ U` or `T ≤ U ≤ object`). + typevars: IndexVec>, + + /// The BDD nodes that appear in any of the constraint sets constructed in this builder. + nodes: IndexVec, + + // Everything below are the memoization tables for the arenas and for our BDD operations. + constraint_cache: FxHashMap, ConstraintId>, + typevar_cache: FxHashMap, TypeVarId>, + node_cache: FxHashMap, + + negate_cache: FxHashMap, + or_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, + and_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, + iff_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, + exists_one_cache: FxHashMap<(NodeId, BoundTypeVarIdentity<'db>), NodeId>, + retain_one_cache: FxHashMap<(NodeId, BoundTypeVarIdentity<'db>), NodeId>, + restrict_one_cache: FxHashMap<(NodeId, ConstraintAssignment), (NodeId, bool)>, + solutions_cache: FxHashMap>>, + simplify_cache: FxHashMap, + + single_sequent_cache: FxHashMap, + pair_sequent_cache: FxHashMap<(ConstraintId, ConstraintId), SequentMap>, } impl<'db> ConstraintSetBuilder<'db> { @@ -588,24 +686,182 @@ impl<'db> ConstraintSetBuilder<'db> { Self::default() } + /// Creates an [`OwnedConstraintSet`], consuming this builder in the process. You provide a + /// callback that constructs a [`ConstraintSet`]. We then package that constraint set up with + /// the storage arenas from this builder. pub(crate) fn into_owned( self, f: impl for<'c> FnOnce(&'c Self) -> ConstraintSet<'db, 'c>, ) -> OwnedConstraintSet<'db> { + // NOTE: We do not store any of the builder's memoization caches in the result. Owned + // constraint sets can only be used by adding them to a new builder. Doing so adds copies + // of the constraints and nodes to the new builder, since they might overlap with + // constraints and nodes that already exist there. That means the memoization caches from + // the original builder aren't relevant to the new builder, and don't need to be retained. let constraint = f(&self); let node = constraint.node; + let storage = self.storage.into_inner(); OwnedConstraintSet { node, - storage: self.storage.into_inner(), + constraints: storage.constraints, + nodes: storage.nodes, + } + } + + /// Loads an [`OwnedConstraintSet`] into this builder. + pub(crate) fn load<'c>( + &'c self, + db: &'db dyn Db, + other: &OwnedConstraintSet<'db>, + ) -> ConstraintSet<'db, 'c> { + // The BDD structure inside a builder depends on the ordering of constraints and typevars + // in the builder's arenas. (The constraint ordering defines the BDD variable ordering, + // while the typevar ordering defines which typevars can be lower/upper bounds of other + // typevars.) There is no guarantee that the `OwnedConstraintSet` and this builder have + // consistent orderings, so we have to just reload everything, standardizing on _this_ + // builder's orderings. That's not the quickest thing in the world, but that's fine, since + // `OwnedConstraintSet` is only used in mdtests, and not in type inference of user code. + + fn rebuild_node<'db>( + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + other: &OwnedConstraintSet<'db>, + cache: &mut FxHashMap, + old_node: NodeId, + ) -> NodeId { + if old_node.is_terminal() { + return old_node; + } + if let Some(remapped) = cache.get(&old_node) { + return *remapped; + } + + let old_interior = other.nodes[old_node]; + let old_constraint = other.constraints[old_interior.constraint]; + let condition = Constraint::new_node( + db, + builder, + old_constraint.typevar, + old_constraint.lower, + old_constraint.upper, + ); + + let if_true = rebuild_node(db, builder, other, cache, old_interior.if_true); + let if_false = rebuild_node(db, builder, other, cache, old_interior.if_false); + let remapped = condition.ite(builder, if_true, if_false); + + cache.insert(old_node, remapped); + remapped } + + // Maps NodeIds in the OwnedConstraintSet to the corresponding NodeIds in this builder. + let mut cache = FxHashMap::default(); + let node = rebuild_node(db, self, other, &mut cache, other.node); + ConstraintSet::from_node(self, node) } - pub(crate) fn load<'c>(&'c self, other: &OwnedConstraintSet<'db>) -> ConstraintSet<'db, 'c> { - // For now, all constraints are still salsa-interned globally, so we can just coerce the - // constraint set to consider ourselves as where it's stored. Once we migrate to actually - // storing the constraints in ConstraintSetStorage, this will need to copy the relevant BDD - // nodes from other's storage into ourselves. - ConstraintSet::from_node(self, other.node) + /// Interns a single typevar, giving it a stable order in this builder + fn intern_typevar(&self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { + let identity = typevar.identity(db); + let mut storage = self.storage.borrow_mut(); + if let Some(id) = storage.typevar_cache.get(&identity) { + return *id; + } + let id = storage.typevars.push(identity); + storage.typevar_cache.insert(identity, id); + id + } + + /// Interns all of the typevars mentioned in a type in a stable order. + fn intern_mentioned_typevars_in_type(&self, db: &'db dyn Db, ty: Type<'db>) { + struct InternMentionedTypevars<'a, 'db> { + builder: &'a ConstraintSetBuilder<'db>, + recursion_guard: TypeCollector<'db>, + } + + impl<'db> TypeVisitor<'db> for InternMentionedTypevars<'_, 'db> { + fn should_visit_lazy_type_attributes(&self) -> bool { + false + } + + fn visit_bound_type_var_type( + &self, + db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, + ) { + self.builder.intern_typevar(db, bound_typevar); + walk_bound_type_var_type(db, bound_typevar, self); + } + + fn visit_generic_alias_type(&self, db: &'db dyn Db, alias: GenericAlias<'db>) { + for ty in alias.specialization(db).types(db) { + self.visit_type(db, *ty); + } + } + + fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { + walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); + } + } + + InternMentionedTypevars { + builder: self, + recursion_guard: TypeCollector::default(), + } + .visit_type(db, ty); + } + + /// Interns all of the typevars mentioned in a constraint in a stable order. + fn intern_constraint_typevars( + &self, + db: &'db dyn Db, + typevar: BoundTypeVarInstance<'db>, + lower: Type<'db>, + upper: Type<'db>, + ) { + self.intern_typevar(db, typevar); + self.intern_mentioned_typevars_in_type(db, lower); + self.intern_mentioned_typevars_in_type(db, upper); + } + + fn intern_constraint(&self, db: &'db dyn Db, data: Constraint<'db>) -> ConstraintId { + self.intern_constraint_typevars(db, data.typevar, data.lower, data.upper); + + let mut storage = self.storage.borrow_mut(); + if let Some(id) = storage.constraint_cache.get(&data) { + return *id; + } + let id = storage.constraints.push(data); + storage.constraint_cache.insert(data, id); + id + } + + fn intern_interior_node(&self, data: InteriorNodeData) -> NodeId { + let mut storage = self.storage.borrow_mut(); + if let Some(id) = storage.node_cache.get(&data) { + return *id; + } + let id = storage.nodes.push(data); + storage.node_cache.insert(data, id); + id + } + + fn typevar_id(&self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { + let identity = typevar.identity(db); + self.storage + .borrow() + .typevar_cache + .get(&identity) + .copied() + .expect("typevar should be interned before ordering") + } + + fn constraint_data(&self, constraint: ConstraintId) -> Constraint<'db> { + self.storage.borrow().constraints[constraint] + } + + fn interior_node_data(&self, node: NodeId) -> InteriorNodeData { + self.storage.borrow().nodes[node] } } @@ -619,14 +875,19 @@ impl<'db> BoundTypeVarInstance<'db> { /// any cycles. This particular ordering plays nicely with how we are ordering constraints /// within a BDD — it means that if a typevar has another typevar as a bound, all of the /// constraints that apply to the bound will appear lower in the BDD. - fn can_be_bound_for(self, db: &'db dyn Db, typevar: Self) -> bool { - self.identity(db) > typevar.identity(db) + fn can_be_bound_for( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + typevar: Self, + ) -> bool { + builder.typevar_id(db, self).index() < builder.typevar_id(db, typevar).index() } } #[derive(Clone, Copy, Debug)] enum IntersectionResult<'db> { - Simplified(ConstrainedTypeVar<'db>), + Simplified(Constraint<'db>), CannotSimplify, Disjoint, } @@ -637,29 +898,55 @@ impl IntersectionResult<'_> { } } +/// The index of a bound typevar within a [`ConstraintSetStorage`]. +#[newtype_index] +#[derive(salsa::Update, get_size2::GetSize)] +pub struct TypeVarId; + +/// The index of an individual constraint (i.e. a BDD variable) within a [`ConstraintSetStorage`]. +#[newtype_index] +#[derive(salsa::Update, get_size2::GetSize)] +pub struct ConstraintId; + /// An individual constraint in a constraint set. This restricts a single typevar to be within a /// lower and upper bound. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub(crate) struct ConstrainedTypeVar<'db> { +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] +pub(crate) struct Constraint<'db> { pub(crate) typevar: BoundTypeVarInstance<'db>, pub(crate) lower: Type<'db>, pub(crate) upper: Type<'db>, } -// The Salsa heap is tracked separately. -impl get_size2::GetSize for ConstrainedTypeVar<'_> {} +impl ConstraintId { + fn new<'db>( + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + typevar: BoundTypeVarInstance<'db>, + lower: Type<'db>, + upper: Type<'db>, + ) -> ConstraintId { + builder.intern_constraint( + db, + Constraint { + typevar, + lower, + upper, + }, + ) + } +} -#[salsa::tracked] -impl<'db> ConstrainedTypeVar<'db> { +impl<'db> Constraint<'db> { /// Returns a new range constraint. /// /// Panics if `lower` and `upper` are not both fully static. fn new_node( db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, mut lower: Type<'db>, mut upper: Type<'db>, - ) -> Node<'db> { + ) -> NodeId { // It's not useful for an upper bound to be an intersection type, or for a lower bound to // be a union type. Because the following equivalences hold, we can break these bounds // apart and create an equivalent BDD with more nodes but simpler constraints. (Fewer, @@ -669,11 +956,11 @@ impl<'db> ConstrainedTypeVar<'db> { // T ≤ (¬α & ¬β) ⇔ (T ≤ ¬α) ∧ (T ≤ ¬β) // (α | β) ≤ T ⇔ (α ≤ T) ∧ (β ≤ T) if let Type::Union(lower_union) = lower { - let mut result = Node::AlwaysTrue; + let mut result = ALWAYS_TRUE; for lower_element in lower_union.elements(db) { result = result.and_with_offset( - db, - ConstrainedTypeVar::new_node(db, typevar, *lower_element, upper), + builder, + Constraint::new_node(db, builder, typevar, *lower_element, upper), ); } return result; @@ -684,17 +971,17 @@ impl<'db> ConstrainedTypeVar<'db> { if let Type::Intersection(upper_intersection) = upper && !upper_intersection.is_simple_negation(db) { - let mut result = Node::AlwaysTrue; + let mut result = ALWAYS_TRUE; for upper_element in upper_intersection.iter_positive(db) { result = result.and_with_offset( - db, - ConstrainedTypeVar::new_node(db, typevar, lower, upper_element), + builder, + Constraint::new_node(db, builder, typevar, lower, upper_element), ); } for upper_element in upper_intersection.iter_negative(db) { result = result.and_with_offset( - db, - ConstrainedTypeVar::new_node(db, typevar, lower, upper_element.negate(db)), + builder, + Constraint::new_node(db, builder, typevar, lower, upper_element.negate(db)), ); } return result; @@ -725,11 +1012,11 @@ impl<'db> ConstrainedTypeVar<'db> { }) => { return Node::new_constraint( - db, - ConstrainedTypeVar::new(db, typevar, Type::Never, Type::object()), + builder, + ConstraintId::new(db, builder, typevar, Type::Never, Type::object()), 1, ) - .negate(db); + .negate(builder); } _ => {} } @@ -754,9 +1041,11 @@ impl<'db> ConstrainedTypeVar<'db> { // If `lower ≰ upper`, then the constraint cannot be satisfied, since there is no type that // is both greater than `lower`, and less than `upper`. if !lower.is_constraint_set_assignable_to(db, upper) { - return Node::AlwaysFalse; + return ALWAYS_FALSE; } + builder.intern_constraint_typevars(db, typevar, lower, upper); + // We have an (arbitrary) ordering for typevars. If the upper and/or lower bounds are // typevars, we have to ensure that the bounds are "later" according to that order than the // typevar being constrained. @@ -766,15 +1055,16 @@ impl<'db> ConstrainedTypeVar<'db> { match (lower, upper) { // L ≤ T ≤ L == (T ≤ [L] ≤ T) (Type::TypeVar(lower), Type::TypeVar(upper)) if lower.is_same_typevar_as(db, upper) => { - let (bound, typevar) = if lower.can_be_bound_for(db, typevar) { + let (bound, typevar) = if lower.can_be_bound_for(db, builder, typevar) { (lower, typevar) } else { (typevar, lower) }; Node::new_constraint( - db, - ConstrainedTypeVar::new( + builder, + ConstraintId::new( db, + builder, typevar, Type::TypeVar(bound), Type::TypeVar(bound), @@ -785,60 +1075,67 @@ impl<'db> ConstrainedTypeVar<'db> { // L ≤ T ≤ U == ([L] ≤ T) && (T ≤ [U]) (Type::TypeVar(lower), Type::TypeVar(upper)) - if typevar.can_be_bound_for(db, lower) && typevar.can_be_bound_for(db, upper) => + if typevar.can_be_bound_for(db, builder, lower) + && typevar.can_be_bound_for(db, builder, upper) => { let lower = Node::new_constraint( - db, - ConstrainedTypeVar::new(db, lower, Type::Never, Type::TypeVar(typevar)), + builder, + ConstraintId::new(db, builder, lower, Type::Never, Type::TypeVar(typevar)), 1, ); let upper = Node::new_constraint( - db, - ConstrainedTypeVar::new(db, upper, Type::TypeVar(typevar), Type::object()), + builder, + ConstraintId::new(db, builder, upper, Type::TypeVar(typevar), Type::object()), 1, ); - lower.and(db, upper) + lower.and(builder, upper) } // L ≤ T ≤ U == ([L] ≤ T) && ([T] ≤ U) - (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, lower) => { + (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, builder, lower) => { let lower = Node::new_constraint( - db, - ConstrainedTypeVar::new(db, lower, Type::Never, Type::TypeVar(typevar)), + builder, + ConstraintId::new(db, builder, lower, Type::Never, Type::TypeVar(typevar)), 1, ); let upper = if upper.is_object() { - Node::AlwaysTrue + ALWAYS_TRUE } else { - Self::new_node(db, typevar, Type::Never, upper) + Constraint::new_node(db, builder, typevar, Type::Never, upper) }; - lower.and(db, upper) + lower.and(builder, upper) } // L ≤ T ≤ U == (L ≤ [T]) && (T ≤ [U]) - (_, Type::TypeVar(upper)) if typevar.can_be_bound_for(db, upper) => { + (_, Type::TypeVar(upper)) if typevar.can_be_bound_for(db, builder, upper) => { let lower = if lower.is_never() { - Node::AlwaysTrue + ALWAYS_TRUE } else { - Self::new_node(db, typevar, lower, Type::object()) + Constraint::new_node(db, builder, typevar, lower, Type::object()) }; let upper = Node::new_constraint( - db, - ConstrainedTypeVar::new(db, upper, Type::TypeVar(typevar), Type::object()), + builder, + ConstraintId::new(db, builder, upper, Type::TypeVar(typevar), Type::object()), 1, ); - lower.and(db, upper) + lower.and(builder, upper) } - _ => Node::new_constraint(db, ConstrainedTypeVar::new(db, typevar, lower, upper), 1), + _ => Node::new_constraint( + builder, + ConstraintId::new(db, builder, typevar, lower, upper), + 1, + ), } } +} - fn when_true(self) -> ConstraintAssignment<'db> { +impl ConstraintId { + fn when_true(self) -> ConstraintAssignment { ConstraintAssignment::Positive(self) } - fn when_false(self) -> ConstraintAssignment<'db> { + fn when_false(self) -> ConstraintAssignment { ConstraintAssignment::Negative(self) } @@ -850,12 +1147,12 @@ impl<'db> ConstrainedTypeVar<'db> { /// and working with BDDs. We don't do that, but we have tried to make some simple choices that /// have clear wins. /// - /// In particular, we use the IDs that salsa assigns to each constraint as it is created. This - /// tends to ensure that constraints that are close to each other in the source are also close - /// to each other in the BDD structure. + /// In particular, we use the order that constraints are added to this builder. This gives us + /// an ordering that is stable across runs, and which is not influenced by when and how quickly + /// we analyze the other files in the project. /// /// As an optimization, we also _reverse_ this ordering, so that constraints that appear - /// earlier in the source appear "lower" (closer to the terminal nodes) in the BDD. Since we + /// earlier in the arena appear "lower" (closer to the terminal nodes) in the BDD. Since we /// build up BDDs by combining smaller BDDs (which will have been constructed from expressions /// earlier in the source), this tends to minimize the amount of "node shuffling" that we have /// to do when combining BDDs. @@ -865,8 +1162,8 @@ impl<'db> ConstrainedTypeVar<'db> { /// adjacent in the BDD structure. However, this proved to be counterproductive; we've found /// empirically that we get smaller BDDs with an ordering that is more aligned with source /// order. - fn ordering(self, _db: &'db dyn Db) -> impl Ord { - std::cmp::Reverse(self.as_id()) + fn ordering(self) -> impl Ord { + std::cmp::Reverse(self.index()) } /// Returns whether this constraint implies another — i.e., whether every type that @@ -874,20 +1171,35 @@ impl<'db> ConstrainedTypeVar<'db> { /// /// This is used to simplify how we display constraint sets, by removing redundant constraints /// from a clause. - fn implies(self, db: &'db dyn Db, other: Self) -> bool { - if !self.typevar(db).is_same_typevar_as(db, other.typevar(db)) { + fn implies<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + other: Self, + ) -> bool { + let self_constraint = builder.constraint_data(self); + let other_constraint = builder.constraint_data(other); + if !self_constraint + .typevar + .is_same_typevar_as(db, other_constraint.typevar) + { return false; } - other - .lower(db) - .is_constraint_set_assignable_to(db, self.lower(db)) - && self - .upper(db) - .is_constraint_set_assignable_to(db, other.upper(db)) + other_constraint + .lower + .is_constraint_set_assignable_to(db, self_constraint.lower) + && self_constraint + .upper + .is_constraint_set_assignable_to(db, other_constraint.upper) } /// Returns the intersection of two range constraints, or `None` if the intersection is empty. - fn intersect(self, db: &'db dyn Db, other: Self) -> IntersectionResult<'db> { + fn intersect<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + other: Self, + ) -> IntersectionResult<'db> { /// TODO: For now, we treat some upper bounds as unsimplifiable if they become "too big". /// When intersecting constraints, the upper bounds are also intersected together. If the /// lhs and rhs upper bounds are unions of intersections (e.g. `(a & b) | (c & d)`), then @@ -896,23 +1208,26 @@ impl<'db> ConstrainedTypeVar<'db> { /// that would let us model this case more directly, but for now, we punt. const MAX_UPPER_BOUND_SIZE: usize = 4; - let self_upper = self.upper(db); - let other_upper = other.upper(db); - let estimated_upper_bound_size = self_upper + let self_constraint = builder.constraint_data(self); + let other_constraint = builder.constraint_data(other); + let estimated_upper_bound_size = self_constraint + .upper .union_size(db) - .saturating_mul(other_upper.union_size(db)) + .saturating_mul(other_constraint.upper.union_size(db)) .saturating_mul( - self_upper + self_constraint + .upper .intersection_size(db) - .saturating_add(other_upper.intersection_size(db)), + .saturating_add(other_constraint.upper.intersection_size(db)), ); if estimated_upper_bound_size >= MAX_UPPER_BOUND_SIZE { return IntersectionResult::CannotSimplify; } // (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂)) - let lower = UnionType::from_two_elements(db, self.lower(db), other.lower(db)); - let upper = IntersectionType::from_two_elements(db, self_upper, other_upper); + let lower = UnionType::from_two_elements(db, self_constraint.lower, other_constraint.lower); + let upper = + IntersectionType::from_two_elements(db, self_constraint.upper, other_constraint.upper); // If `lower ≰ upper`, then the intersection is empty, since there is no type that is both // greater than `lower`, and less than `upper`. @@ -926,29 +1241,46 @@ impl<'db> ConstrainedTypeVar<'db> { return IntersectionResult::CannotSimplify; } - IntersectionResult::Simplified(Self::new(db, self.typevar(db), lower, upper)) + IntersectionResult::Simplified(Constraint { + typevar: self_constraint.typevar, + lower, + upper, + }) } - pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { - self.display_inner(db, false) + pub(crate) fn display<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + ) -> impl Display { + self.display_inner(db, builder, false) } - fn display_negated(self, db: &'db dyn Db) -> impl Display { - self.display_inner(db, true) + fn display_negated<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + ) -> impl Display { + self.display_inner(db, builder, true) } - fn display_inner(self, db: &'db dyn Db, negated: bool) -> impl Display { + fn display_inner<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + negated: bool, + ) -> impl Display { struct DisplayConstrainedTypeVar<'db> { - constraint: ConstrainedTypeVar<'db>, + constraint: Constraint<'db>, negated: bool, db: &'db dyn Db, } impl Display for DisplayConstrainedTypeVar<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let lower = self.constraint.lower(self.db); - let upper = self.constraint.upper(self.db); - let typevar = self.constraint.typevar(self.db); + let lower = self.constraint.lower; + let upper = self.constraint.upper; + let typevar = self.constraint.typevar; if lower.is_equivalent_to(self.db, upper) { // If this typevar is equivalent to another, output the constraint in a // consistent alphabetical order, regardless of the salsa ordering that we are @@ -1004,20 +1336,22 @@ impl<'db> ConstrainedTypeVar<'db> { } DisplayConstrainedTypeVar { - constraint: self, + constraint: builder.constraint_data(self), negated, db, } } } -/// A BDD node. +/// The index of a BDD node within a [`ConstraintSetBuilder`]. /// /// The "variables" of a constraint set BDD are individual constraints, represented by an interned -/// [`ConstrainedTypeVar`]. +/// [`Constraint`]. /// -/// Terminal nodes (`false` and `true`) have their own dedicated enum variants. The -/// [`Interior`][InteriorNode] variant represents interior nodes. +/// Terminal nodes (`false` and `true`) have hard-coded IDs. Interior nodes are stored in a +/// [`ConstraintSetBuilder`], and are represented by the index into the storage array. By +/// construction, interior nodes can only refer to nodes with smaller indexes (since the nodes that +/// outgoing edges point at must already exist). /// /// BDD nodes are _quasi-reduced_, which means that there are no duplicate nodes (which we handle /// via Salsa interning). Unlike the typical BDD representation, which is (fully) reduced, we do @@ -1025,7 +1359,7 @@ impl<'db> ConstrainedTypeVar<'db> { /// means that our BDDs "remember" all of the individual constraints that they were created with. /// /// BDD nodes are also _ordered_, meaning that every path from the root of a BDD to a terminal node -/// visits variables in the same order. [`ConstrainedTypeVar::ordering`] defines the variable +/// visits variables in the same order. [`ConstraintId::ordering`] defines the variable /// ordering that we use for constraint set BDDs. /// /// In addition to this BDD variable ordering, we also track a `source_order` for each individual @@ -1035,252 +1369,345 @@ impl<'db> ConstrainedTypeVar<'db> { /// cannot use this ordering as our BDD variable ordering, since we calculate it from already /// constructed BDDs, and we need the BDD variable ordering to be fixed and available before /// construction starts.) -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] -enum Node<'db> { - AlwaysFalse, +#[derive(Clone, Copy, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] +struct NodeId(u32); + +/// A special ID that is used for an "always true" / "always visible" constraint. +const ALWAYS_TRUE: NodeId = NodeId(0xffff_ffff); + +/// A special ID that is used for an "always false" / "never visible" constraint. +const ALWAYS_FALSE: NodeId = NodeId(0xffff_fffe); + +const SMALLEST_TERMINAL: NodeId = ALWAYS_FALSE; + +enum Node { AlwaysTrue, - Interior(InteriorNode<'db>), + AlwaysFalse, + Interior(InteriorNode), } -impl<'db> Node<'db> { +impl NodeId { /// Creates a new BDD node, ensuring that it is quasi-reduced. fn new( - db: &'db dyn Db, - constraint: ConstrainedTypeVar<'db>, - if_true: Node<'db>, - if_false: Node<'db>, + builder: &ConstraintSetBuilder<'_>, + constraint: ConstraintId, + if_true: NodeId, + if_false: NodeId, source_order: usize, - ) -> Self { - debug_assert!((if_true.root_constraint(db)).is_none_or(|root_constraint| { - root_constraint.ordering(db) > constraint.ordering(db) - })); + ) -> NodeId { debug_assert!( - (if_false.root_constraint(db)).is_none_or(|root_constraint| { - root_constraint.ordering(db) > constraint.ordering(db) - }) + if_true + .root_constraint(builder) + .is_none_or(|root_constraint| { + root_constraint.ordering() > constraint.ordering() + }) + ); + debug_assert!( + if_false + .root_constraint(builder) + .is_none_or(|root_constraint| { + root_constraint.ordering() > constraint.ordering() + }) ); - if if_true == Node::AlwaysFalse && if_false == Node::AlwaysFalse { - return Node::AlwaysFalse; + if if_true == ALWAYS_FALSE && if_false == ALWAYS_FALSE { + return ALWAYS_FALSE; } let max_source_order = source_order - .max(if_true.max_source_order(db)) - .max(if_false.max_source_order(db)); - Self::Interior(InteriorNode::new( - db, + .max(if_true.max_source_order(builder)) + .max(if_false.max_source_order(builder)); + builder.intern_interior_node(InteriorNodeData { constraint, if_true, if_false, source_order, max_source_order, - )) + }) } +} +impl Node { /// Creates a new BDD node for an individual constraint. (The BDD will evaluate to `true` when /// the constraint holds, and to `false` when it does not.) fn new_constraint( - db: &'db dyn Db, - constraint: ConstrainedTypeVar<'db>, + builder: &ConstraintSetBuilder<'_>, + constraint: ConstraintId, source_order: usize, - ) -> Self { - Self::Interior(InteriorNode::new( - db, + ) -> NodeId { + builder.intern_interior_node(InteriorNodeData { constraint, - Node::AlwaysTrue, - Node::AlwaysFalse, - source_order, + if_true: ALWAYS_TRUE, + if_false: ALWAYS_FALSE, source_order, - )) + max_source_order: source_order, + }) } /// Creates a new BDD node for a positive or negative individual constraint. (For a positive /// constraint, this returns the same BDD node as [`new_constraint`][Self::new_constraint]. For /// a negative constraint, it returns the negation of that BDD node.) fn new_satisfied_constraint( - db: &'db dyn Db, - constraint: ConstraintAssignment<'db>, + builder: &ConstraintSetBuilder<'_>, + constraint: ConstraintAssignment, source_order: usize, - ) -> Self { + ) -> NodeId { match constraint { - ConstraintAssignment::Positive(constraint) => Self::Interior(InteriorNode::new( - db, - constraint, - Node::AlwaysTrue, - Node::AlwaysFalse, - source_order, - source_order, - )), - ConstraintAssignment::Negative(constraint) => Self::Interior(InteriorNode::new( - db, - constraint, - Node::AlwaysFalse, - Node::AlwaysTrue, - source_order, - source_order, - )), + ConstraintAssignment::Positive(constraint) => { + builder.intern_interior_node(InteriorNodeData { + constraint, + if_true: ALWAYS_TRUE, + if_false: ALWAYS_FALSE, + source_order, + max_source_order: source_order, + }) + } + ConstraintAssignment::Negative(constraint) => { + builder.intern_interior_node(InteriorNodeData { + constraint, + if_true: ALWAYS_FALSE, + if_false: ALWAYS_TRUE, + source_order, + max_source_order: source_order, + }) + } + } + } +} + +impl NodeId { + fn node(self) -> Node { + match self { + ALWAYS_TRUE => Node::AlwaysTrue, + ALWAYS_FALSE => Node::AlwaysFalse, + _ => Node::Interior(InteriorNode(self)), } } + fn is_terminal(self) -> bool { + self.0 >= SMALLEST_TERMINAL.0 + } + /// Returns the BDD variable of the root node of this BDD, or `None` if this BDD is a terminal /// node. - fn root_constraint(self, db: &'db dyn Db) -> Option> { - match self { - Node::Interior(interior) => Some(interior.constraint(db)), - _ => None, + fn root_constraint(self, builder: &ConstraintSetBuilder<'_>) -> Option { + if self.is_terminal() { + return None; } + let interior = builder.interior_node_data(self); + Some(interior.constraint) } - fn max_source_order(self, db: &'db dyn Db) -> usize { - match self { - Node::Interior(interior) => interior.max_source_order(db), - Node::AlwaysTrue | Node::AlwaysFalse => 0, + fn max_source_order(self, builder: &ConstraintSetBuilder<'_>) -> usize { + if self.is_terminal() { + return 0; } + let interior = builder.interior_node_data(self); + interior.max_source_order } /// Returns a copy of this BDD node with all `source_order`s adjusted by the given amount. - fn with_adjusted_source_order(self, db: &'db dyn Db, delta: usize) -> Self { + fn with_adjusted_source_order(self, builder: &ConstraintSetBuilder<'_>, delta: usize) -> Self { if delta == 0 { return self; } - match self { - Node::AlwaysTrue => Node::AlwaysTrue, - Node::AlwaysFalse => Node::AlwaysFalse, - Node::Interior(interior) => Node::new( - db, - interior.constraint(db), - interior.if_true(db).with_adjusted_source_order(db, delta), - interior.if_false(db).with_adjusted_source_order(db, delta), - interior.source_order(db) + delta, - ), + match self.node() { + Node::AlwaysTrue | Node::AlwaysFalse => self, + Node::Interior(_) => { + let interior = builder.interior_node_data(self); + NodeId::new( + builder, + interior.constraint, + interior.if_true.with_adjusted_source_order(builder, delta), + interior.if_false.with_adjusted_source_order(builder, delta), + interior.source_order + delta, + ) + } } } - fn for_each_path(self, db: &'db dyn Db, mut f: impl FnMut(&PathAssignments<'db>)) { - match self { + fn for_each_path<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + mut f: impl FnMut(&PathAssignments), + ) { + match self.node() { Node::AlwaysTrue => {} Node::AlwaysFalse => {} Node::Interior(interior) => { - let mut path = interior.path_assignments(db); - self.for_each_path_inner(db, &mut f, &mut path); + let mut path = interior.path_assignments(builder); + self.for_each_path_inner(db, builder, &mut f, &mut path); } } } - fn for_each_path_inner( + fn for_each_path_inner<'db>( self, db: &'db dyn Db, - f: &mut dyn FnMut(&PathAssignments<'db>), - path: &mut PathAssignments<'db>, + builder: &ConstraintSetBuilder<'db>, + f: &mut dyn FnMut(&PathAssignments), + path: &mut PathAssignments, ) { - match self { + match self.node() { Node::AlwaysTrue => f(path), Node::AlwaysFalse => {} - Node::Interior(interior) => { - let constraint = interior.constraint(db); - let source_order = interior.source_order(db); - path.walk_edge(db, constraint.when_true(), source_order, |path, _| { - interior.if_true(db).for_each_path_inner(db, f, path); - }); - path.walk_edge(db, constraint.when_false(), source_order, |path, _| { - interior.if_false(db).for_each_path_inner(db, f, path); - }); + Node::Interior(_) => { + let interior = builder.interior_node_data(self); + path.walk_edge( + db, + builder, + interior.constraint.when_true(), + interior.source_order, + |path, _| interior.if_true.for_each_path_inner(db, builder, f, path), + ); + path.walk_edge( + db, + builder, + interior.constraint.when_false(), + interior.source_order, + |path, _| interior.if_false.for_each_path_inner(db, builder, f, path), + ); } } } /// Returns whether this BDD represent the constant function `true`. - fn is_always_satisfied(self, db: &'db dyn Db) -> bool { - match self { + fn is_always_satisfied<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + ) -> bool { + match self.node() { Node::AlwaysTrue => true, Node::AlwaysFalse => false, Node::Interior(interior) => { - let mut path = interior.path_assignments(db); - self.is_always_satisfied_inner(db, &mut path) + let mut path = interior.path_assignments(builder); + self.is_always_satisfied_inner(db, builder, &mut path) } } } - fn is_always_satisfied_inner(self, db: &'db dyn Db, path: &mut PathAssignments<'db>) -> bool { - match self { + fn is_always_satisfied_inner<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + path: &mut PathAssignments, + ) -> bool { + match self.node() { Node::AlwaysTrue => true, Node::AlwaysFalse => false, - Node::Interior(interior) => { + Node::Interior(_) => { // walk_edge will return None if this node's constraint (or anything we can derive // from it) causes the if_true edge to become impossible. We want to ignore // impossible paths, and so we treat them as passing the "always satisfied" check. - let constraint = interior.constraint(db); - let source_order = interior.source_order(db); + let interior = builder.interior_node_data(self); let true_always_satisfied = path - .walk_edge(db, constraint.when_true(), source_order, |path, _| { - interior.if_true(db).is_always_satisfied_inner(db, path) - }) + .walk_edge( + db, + builder, + interior.constraint.when_true(), + interior.source_order, + |path, _| { + interior + .if_true + .is_always_satisfied_inner(db, builder, path) + }, + ) .unwrap_or(true); if !true_always_satisfied { return false; } // Ditto for the if_false branch - path.walk_edge(db, constraint.when_false(), source_order, |path, _| { - interior.if_false(db).is_always_satisfied_inner(db, path) - }) + path.walk_edge( + db, + builder, + interior.constraint.when_false(), + interior.source_order, + |path, _| { + interior + .if_false + .is_always_satisfied_inner(db, builder, path) + }, + ) .unwrap_or(true) } } } /// Returns whether this BDD represent the constant function `false`. - fn is_never_satisfied(self, db: &'db dyn Db) -> bool { - match self { + fn is_never_satisfied<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> bool { + match self.node() { Node::AlwaysTrue => false, Node::AlwaysFalse => true, Node::Interior(interior) => { - let mut path = interior.path_assignments(db); - self.is_never_satisfied_inner(db, &mut path) + let mut path = interior.path_assignments(builder); + self.is_never_satisfied_inner(db, builder, &mut path) } } } - fn is_never_satisfied_inner(self, db: &'db dyn Db, path: &mut PathAssignments<'db>) -> bool { - match self { + fn is_never_satisfied_inner<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + path: &mut PathAssignments, + ) -> bool { + match self.node() { Node::AlwaysTrue => false, Node::AlwaysFalse => true, - Node::Interior(interior) => { + Node::Interior(_) => { // walk_edge will return None if this node's constraint (or anything we can derive // from it) causes the if_true edge to become impossible. We want to ignore // impossible paths, and so we treat them as passing the "never satisfied" check. - let constraint = interior.constraint(db); - let source_order = interior.source_order(db); + let interior = builder.interior_node_data(self); let true_never_satisfied = path - .walk_edge(db, constraint.when_true(), source_order, |path, _| { - interior.if_true(db).is_never_satisfied_inner(db, path) - }) + .walk_edge( + db, + builder, + interior.constraint.when_true(), + interior.source_order, + |path, _| interior.if_true.is_never_satisfied_inner(db, builder, path), + ) .unwrap_or(true); if !true_never_satisfied { return false; } // Ditto for the if_false branch - path.walk_edge(db, constraint.when_false(), source_order, |path, _| { - interior.if_false(db).is_never_satisfied_inner(db, path) - }) + path.walk_edge( + db, + builder, + interior.constraint.when_false(), + interior.source_order, + |path, _| { + interior + .if_false + .is_never_satisfied_inner(db, builder, path) + }, + ) .unwrap_or(true) } } } - fn solutions(self, db: &'db dyn Db) -> Solutions<'db> { - match self { + fn solutions<'db, 'c>( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + ) -> Solutions<'db, 'c> { + match self.node() { Node::AlwaysTrue => Solutions::Unconstrained, Node::AlwaysFalse => Solutions::Unsatisfiable, - Node::Interior(interior) => interior.solutions(db), + Node::Interior(interior) => interior.solutions(db, builder), } } /// Returns the negation of this BDD. - fn negate(self, db: &'db dyn Db) -> Self { - match self { - Node::AlwaysTrue => Node::AlwaysFalse, - Node::AlwaysFalse => Node::AlwaysTrue, - Node::Interior(interior) => interior.negate(db), + fn negate(self, builder: &ConstraintSetBuilder<'_>) -> Self { + match self.node() { + Node::AlwaysTrue => ALWAYS_FALSE, + Node::AlwaysFalse => ALWAYS_TRUE, + Node::Interior(interior) => interior.negate(builder), } } @@ -1288,7 +1715,7 @@ impl<'db> Node<'db> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - fn or_with_offset(self, db: &'db dyn Db, other: Self) -> Self { + fn or_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { // To ensure that `self` appears before `other` in `source_order`, we add the maximum // `source_order` of the lhs to all of the `source_order`s in the rhs. // @@ -1296,35 +1723,46 @@ impl<'db> Node<'db> { // avoid all of the extra work in the calls to with_adjusted_source_order, and apply the // adjustment lazily when walking a BDD tree. (ditto below in the other _with_offset // methods) - let other_offset = self.max_source_order(db); - self.or_inner(db, other, other_offset) + let other_offset = self.max_source_order(builder); + self.or_inner(builder, other, other_offset) } - fn or(self, db: &'db dyn Db, other: Self) -> Self { - self.or_inner(db, other, 0) + fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { + self.or_inner(builder, other, 0) } - fn or_inner(self, db: &'db dyn Db, other: Self, other_offset: usize) -> Self { - match (self, other) { - (Node::AlwaysTrue, Node::AlwaysTrue) => Node::AlwaysTrue, - (Node::AlwaysTrue, Node::Interior(other_interior)) => Node::new( - db, - other_interior.constraint(db), - Node::AlwaysTrue, - Node::AlwaysTrue, - other_interior.source_order(db) + other_offset, - ), - (Node::Interior(self_interior), Node::AlwaysTrue) => Node::new( - db, - self_interior.constraint(db), - Node::AlwaysTrue, - Node::AlwaysTrue, - self_interior.source_order(db), - ), - (Node::AlwaysFalse, _) => other.with_adjusted_source_order(db, other_offset), + fn or_inner( + self, + builder: &ConstraintSetBuilder<'_>, + other: Self, + other_offset: usize, + ) -> Self { + match (self.node(), other.node()) { + (Node::AlwaysTrue, Node::AlwaysTrue) => ALWAYS_TRUE, + (Node::AlwaysTrue, Node::Interior(_)) => { + let other_interior = builder.interior_node_data(other); + NodeId::new( + builder, + other_interior.constraint, + ALWAYS_TRUE, + ALWAYS_TRUE, + other_interior.source_order + other_offset, + ) + } + (Node::Interior(_), Node::AlwaysTrue) => { + let self_interior = builder.interior_node_data(self); + NodeId::new( + builder, + self_interior.constraint, + ALWAYS_TRUE, + ALWAYS_TRUE, + self_interior.source_order, + ) + } + (Node::AlwaysFalse, _) => other.with_adjusted_source_order(builder, other_offset), (_, Node::AlwaysFalse) => self, (Node::Interior(self_interior), Node::Interior(other_interior)) => { - self_interior.or(db, other_interior, other_offset) + self_interior.or(builder, other_interior, other_offset) } } } @@ -1347,12 +1785,13 @@ impl<'db> Node<'db> { /// that has no effect (`0 ∨ a = a`). It is returned if the iterator is empty. The "one" is the /// value that saturates (`1 ∨ a = 1`). We use this to short-circuit; if any element BDD or any /// intermediate result evaluates to "one", we can return early. - fn tree_fold( + fn tree_fold<'db>( db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, nodes: impl Iterator, zero: Self, - is_one: impl Fn(Self, &'db dyn Db) -> bool, - mut combine: impl FnMut(Self, &'db dyn Db, Self) -> Self, + is_one: impl Fn(Self, &'db dyn Db, &ConstraintSetBuilder<'db>) -> bool, + mut combine: impl FnMut(Self, &ConstraintSetBuilder<'db>, Self) -> Self, ) -> Self { // To implement the "linear" shape described above, we could collect the iterator elements // into a vector, and then use the fold at the bottom of this method to combine the @@ -1378,9 +1817,9 @@ impl<'db> Node<'db> { // // We use a SmallVec for the accumulator so that we don't have to spill over to the heap // until the iterator passes 256 elements. - let mut accumulator: SmallVec<[(Node<'db>, u8); 8]> = SmallVec::default(); + let mut accumulator: SmallVec<[(NodeId, u8); 8]> = SmallVec::default(); for node in nodes { - if is_one(node, db) { + if is_one(node, db, builder) { return node; } @@ -1390,8 +1829,8 @@ impl<'db> Node<'db> { .is_some_and(|(_, existing)| *existing == depth) { let (existing, _) = accumulator.pop().expect("accumulator should not be empty"); - node = combine(existing, db, node); - if is_one(node, db) { + node = combine(existing, builder, node); + if is_one(node, db, builder) { return node; } depth += 1; @@ -1404,24 +1843,34 @@ impl<'db> Node<'db> { // produce the overall result. accumulator .into_iter() - .fold(zero, |result, (node, _)| combine(result, db, node)) + .fold(zero, |result, (node, _)| combine(result, builder, node)) } - fn distributed_or(db: &'db dyn Db, nodes: impl Iterator>) -> Self { + fn distributed_or<'db>( + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + nodes: impl Iterator, + ) -> Self { Self::tree_fold( db, + builder, nodes, - Node::AlwaysFalse, + ALWAYS_FALSE, Self::is_always_satisfied, Self::or_with_offset, ) } - fn distributed_and(db: &'db dyn Db, nodes: impl Iterator>) -> Self { + fn distributed_and<'db>( + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + nodes: impl Iterator, + ) -> Self { Self::tree_fold( db, + builder, nodes, - Node::AlwaysTrue, + ALWAYS_TRUE, Self::is_never_satisfied, Self::and_with_offset, ) @@ -1431,45 +1880,56 @@ impl<'db> Node<'db> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - fn and_with_offset(self, db: &'db dyn Db, other: Self) -> Self { + fn and_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { // To ensure that `self` appears before `other` in `source_order`, we add the maximum // `source_order` of the lhs to all of the `source_order`s in the rhs. - let other_offset = self.max_source_order(db); - self.and_inner(db, other, other_offset) + let other_offset = self.max_source_order(builder); + self.and_inner(builder, other, other_offset) } - fn and(self, db: &'db dyn Db, other: Self) -> Self { - self.and_inner(db, other, 0) + fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { + self.and_inner(builder, other, 0) } - fn and_inner(self, db: &'db dyn Db, other: Self, other_offset: usize) -> Self { - match (self, other) { - (Node::AlwaysFalse, Node::AlwaysFalse) => Node::AlwaysFalse, - (Node::AlwaysFalse, Node::Interior(other_interior)) => Node::new( - db, - other_interior.constraint(db), - Node::AlwaysFalse, - Node::AlwaysFalse, - other_interior.source_order(db) + other_offset, - ), - (Node::Interior(self_interior), Node::AlwaysFalse) => Node::new( - db, - self_interior.constraint(db), - Node::AlwaysFalse, - Node::AlwaysFalse, - self_interior.source_order(db), - ), - (Node::AlwaysTrue, _) => other.with_adjusted_source_order(db, other_offset), + fn and_inner( + self, + builder: &ConstraintSetBuilder<'_>, + other: Self, + other_offset: usize, + ) -> Self { + match (self.node(), other.node()) { + (Node::AlwaysFalse, Node::AlwaysFalse) => ALWAYS_FALSE, + (Node::AlwaysFalse, Node::Interior(_)) => { + let other_interior = builder.interior_node_data(other); + NodeId::new( + builder, + other_interior.constraint, + ALWAYS_FALSE, + ALWAYS_FALSE, + other_interior.source_order + other_offset, + ) + } + (Node::Interior(_), Node::AlwaysFalse) => { + let self_interior = builder.interior_node_data(self); + NodeId::new( + builder, + self_interior.constraint, + ALWAYS_FALSE, + ALWAYS_FALSE, + self_interior.source_order, + ) + } + (Node::AlwaysTrue, _) => other.with_adjusted_source_order(builder, other_offset), (_, Node::AlwaysTrue) => self, (Node::Interior(self_interior), Node::Interior(other_interior)) => { - self_interior.and(db, other_interior, other_offset) + self_interior.and(builder, other_interior, other_offset) } } } - fn implies(self, db: &'db dyn Db, other: Self) -> Self { + fn implies(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { // p → q == ¬p ∨ q - self.negate(db).or(db, other) + self.negate(builder).or(builder, other) } /// Returns a new BDD that evaluates to `true` when both input BDDs evaluate to the same @@ -1477,51 +1937,68 @@ impl<'db> Node<'db> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - fn iff_with_offset(self, db: &'db dyn Db, other: Self) -> Self { + fn iff_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { // To ensure that `self` appears before `other` in `source_order`, we add the maximum // `source_order` of the lhs to all of the `source_order`s in the rhs. - let other_offset = self.max_source_order(db); - self.iff_inner(db, other, other_offset) + let other_offset = self.max_source_order(builder); + self.iff_inner(builder, other, other_offset) } - fn iff(self, db: &'db dyn Db, other: Self) -> Self { - self.iff_inner(db, other, 0) + fn iff(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { + self.iff_inner(builder, other, 0) } - fn iff_inner(self, db: &'db dyn Db, other: Self, other_offset: usize) -> Self { - match (self, other) { + fn iff_inner( + self, + builder: &ConstraintSetBuilder<'_>, + other: Self, + other_offset: usize, + ) -> Self { + match (self.node(), other.node()) { (Node::AlwaysFalse, Node::AlwaysFalse) | (Node::AlwaysTrue, Node::AlwaysTrue) => { - Node::AlwaysTrue + ALWAYS_TRUE } (Node::AlwaysTrue, Node::AlwaysFalse) | (Node::AlwaysFalse, Node::AlwaysTrue) => { - Node::AlwaysFalse + ALWAYS_FALSE } - (Node::AlwaysTrue | Node::AlwaysFalse, Node::Interior(interior)) => Node::new( - db, - interior.constraint(db), - self.iff_inner(db, interior.if_true(db), other_offset), - self.iff_inner(db, interior.if_false(db), other_offset), - interior.source_order(db) + other_offset, - ), - (Node::Interior(interior), Node::AlwaysTrue | Node::AlwaysFalse) => Node::new( - db, - interior.constraint(db), - interior.if_true(db).iff_inner(db, other, other_offset), - interior.if_false(db).iff_inner(db, other, other_offset), - interior.source_order(db), - ), - (Node::Interior(a), Node::Interior(b)) => a.iff(db, b, other_offset), + (Node::AlwaysTrue | Node::AlwaysFalse, Node::Interior(_)) => { + let interior = builder.interior_node_data(other); + NodeId::new( + builder, + interior.constraint, + self.iff_inner(builder, interior.if_true, other_offset), + self.iff_inner(builder, interior.if_false, other_offset), + interior.source_order + other_offset, + ) + } + (Node::Interior(_), Node::AlwaysTrue | Node::AlwaysFalse) => { + let interior = builder.interior_node_data(self); + NodeId::new( + builder, + interior.constraint, + interior.if_true.iff_inner(builder, other, other_offset), + interior.if_false.iff_inner(builder, other, other_offset), + interior.source_order, + ) + } + (Node::Interior(a), Node::Interior(b)) => a.iff(builder, b, other_offset), } } /// Returns the `if-then-else` of three BDDs: when `self` evaluates to `true`, it returns what /// `then_node` evaluates to; otherwise it returns what `else_node` evaluates to. - fn ite(self, db: &'db dyn Db, then_node: Self, else_node: Self) -> Self { - self.and(db, then_node) - .or(db, self.negate(db).and(db, else_node)) + fn ite(self, builder: &ConstraintSetBuilder<'_>, then_node: Self, else_node: Self) -> Self { + self.and(builder, then_node) + .or(builder, self.negate(builder).and(builder, else_node)) } - fn implies_subtype_of(self, db: &'db dyn Db, lhs: Type<'db>, rhs: Type<'db>) -> Self { + fn implies_subtype_of<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + lhs: Type<'db>, + rhs: Type<'db>, + ) -> Self { // When checking subtyping involving a typevar, we can turn the subtyping check into a // constraint (i.e, "is `T` a subtype of `int` becomes the constraint `T ≤ int`), and then // check when the BDD implies that constraint. @@ -1531,14 +2008,16 @@ impl<'db> Node<'db> { // perform. So we have to take the appropriate materialization when translating the check // into a constraint. let constraint = match (lhs, rhs) { - (Type::TypeVar(bound_typevar), _) => ConstrainedTypeVar::new_node( + (Type::TypeVar(bound_typevar), _) => Constraint::new_node( db, + builder, bound_typevar, Type::Never, rhs.bottom_materialization(db), ), - (_, Type::TypeVar(bound_typevar)) => ConstrainedTypeVar::new_node( + (_, Type::TypeVar(bound_typevar)) => Constraint::new_node( db, + builder, bound_typevar, lhs.top_materialization(db), Type::object(), @@ -1546,44 +2025,50 @@ impl<'db> Node<'db> { _ => panic!("at least one type should be a typevar"), }; - self.implies(db, constraint) + self.implies(builder, constraint) } - fn satisfied_by_all_typevars( + fn satisfied_by_all_typevars<'db>( self, db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, ) -> bool { - match self { + match self.node() { Node::AlwaysTrue => return true, Node::AlwaysFalse => return false, Node::Interior(_) => {} } let mut typevars = FxHashSet::default(); - self.for_each_constraint(db, &mut |constraint, _| { - typevars.insert(constraint.typevar(db)); + self.for_each_constraint(builder, &mut |constraint, _| { + let constraint = builder.constraint_data(constraint); + typevars.insert(constraint.typevar); }); // Returns if some specialization satisfies this constraint set. - let some_specialization_satisfies = move |specializations: Node<'db>| { - let when_satisfied = specializations.implies(db, self).and(db, specializations); - !when_satisfied.is_never_satisfied(db) + let some_specialization_satisfies = move |specializations: NodeId| { + let when_satisfied = specializations + .implies(builder, self) + .and(builder, specializations); + !when_satisfied.is_never_satisfied(db, builder) }; // Returns if all specializations satisfy this constraint set. - let all_specializations_satisfy = move |specializations: Node<'db>| { - let when_satisfied = specializations.implies(db, self).and(db, specializations); + let all_specializations_satisfy = move |specializations: NodeId| { + let when_satisfied = specializations + .implies(builder, self) + .and(builder, specializations); when_satisfied - .iff(db, specializations) - .is_always_satisfied(db) + .iff(builder, specializations) + .is_always_satisfied(db, builder) }; for typevar in typevars { if typevar.is_inferable(db, inferable) { // If the typevar is in inferable position, we need to verify that some valid // specialization satisfies the constraint set. - let valid_specializations = typevar.valid_specializations(db); + let valid_specializations = typevar.valid_specializations(db, builder); if !some_specialization_satisfies(valid_specializations) { return false; } @@ -1600,7 +2085,7 @@ impl<'db> Node<'db> { // constraint to refer to the synthetic typevar instead of the original gradual // constraint. let (static_specializations, gradual_constraints) = - typevar.required_specializations(db); + typevar.required_specializations(db, builder); if !all_specializations_satisfy(static_specializations) { return false; } @@ -1618,36 +2103,45 @@ impl<'db> Node<'db> { /// Returns a new BDD that is the _existential abstraction_ of `self` for a set of typevars. /// The result will return true whenever `self` returns true for _any_ assignment of those /// typevars. The result will not contain any constraints that mention those typevars. - fn exists( + fn exists<'db>( self, db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, bound_typevars: impl IntoIterator>, ) -> Self { bound_typevars .into_iter() .fold(self, |abstracted, bound_typevar| { - abstracted.exists_one(db, bound_typevar) + abstracted.exists_one(db, builder, bound_typevar) }) } - fn exists_one(self, db: &'db dyn Db, bound_typevar: BoundTypeVarIdentity<'db>) -> Self { - match self { - Node::AlwaysTrue => Node::AlwaysTrue, - Node::AlwaysFalse => Node::AlwaysFalse, - Node::Interior(interior) => interior.exists_one(db, bound_typevar), + fn exists_one<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + bound_typevar: BoundTypeVarIdentity<'db>, + ) -> Self { + match self.node() { + Node::AlwaysTrue => ALWAYS_TRUE, + Node::AlwaysFalse => ALWAYS_FALSE, + Node::Interior(interior) => interior.exists_one(db, builder, bound_typevar), } } - fn abstract_one_inner( + fn abstract_one_inner<'db>( self, db: &'db dyn Db, - should_remove: &mut dyn FnMut(ConstrainedTypeVar<'db>) -> bool, - path: &mut PathAssignments<'db>, + builder: &ConstraintSetBuilder<'db>, + should_remove: &mut dyn FnMut(ConstraintId) -> bool, + path: &mut PathAssignments, ) -> Self { - match self { - Node::AlwaysTrue => Node::AlwaysTrue, - Node::AlwaysFalse => Node::AlwaysFalse, - Node::Interior(interior) => interior.abstract_one_inner(db, should_remove, path), + match self.node() { + Node::AlwaysTrue => ALWAYS_TRUE, + Node::AlwaysFalse => ALWAYS_FALSE, + Node::Interior(interior) => { + interior.abstract_one_inner(db, builder, should_remove, path) + } } } @@ -1656,15 +2150,16 @@ impl<'db> Node<'db> { /// will not be present in the result.) /// /// Also returns whether _all_ of the restricted variables appeared in the BDD. - fn restrict( + fn restrict<'db>( self, db: &'db dyn Db, - assignment: impl IntoIterator>, + builder: &ConstraintSetBuilder<'db>, + assignment: impl IntoIterator, ) -> (Self, bool) { assignment .into_iter() .fold((self, true), |(restricted, found), assignment| { - let (restricted, found_this) = restricted.restrict_one(db, assignment); + let (restricted, found_this) = restricted.restrict_one(db, builder, assignment); (restricted, found && found_this) }) } @@ -1674,30 +2169,36 @@ impl<'db> Node<'db> { /// will not be present in the result.) /// /// Also returns whether the restricted variable appeared in the BDD. - fn restrict_one(self, db: &'db dyn Db, assignment: ConstraintAssignment<'db>) -> (Self, bool) { - match self { - Node::AlwaysTrue => (Node::AlwaysTrue, false), - Node::AlwaysFalse => (Node::AlwaysFalse, false), - Node::Interior(interior) => interior.restrict_one(db, assignment), + fn restrict_one<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + assignment: ConstraintAssignment, + ) -> (Self, bool) { + match self.node() { + Node::AlwaysTrue | Node::AlwaysFalse => (self, false), + Node::Interior(interior) => interior.restrict_one(db, builder, assignment), } } /// Returns a new BDD with any occurrence of `left ∧ right` replaced with `replacement`. - fn substitute_intersection( + #[expect(clippy::too_many_arguments)] + fn substitute_intersection<'db>( self, db: &'db dyn Db, - left: ConstraintAssignment<'db>, + builder: &ConstraintSetBuilder<'db>, + left: ConstraintAssignment, left_source_order: usize, - right: ConstraintAssignment<'db>, + right: ConstraintAssignment, right_source_order: usize, - replacement: Node<'db>, + replacement: NodeId, ) -> Self { // We perform a Shannon expansion to find out what the input BDD evaluates to when: // - left and right are both true // - left is false // - left is true and right is false // This covers the entire truth table of `left ∧ right`. - let (when_left_and_right, both_found) = self.restrict(db, [left, right]); + let (when_left_and_right, both_found) = self.restrict(db, builder, [left, right]); if !both_found { // If left and right are not both present in the input BDD, we should not even attempt // the substitution, since the Shannon expansion might introduce the missing variables! @@ -1705,8 +2206,8 @@ impl<'db> Node<'db> { // with the input. return self; } - let (when_not_left, _) = self.restrict(db, [left.negated()]); - let (when_left_but_not_right, _) = self.restrict(db, [left, right.negated()]); + let (when_not_left, _) = self.restrict(db, builder, [left.negated()]); + let (when_left_but_not_right, _) = self.restrict(db, builder, [left, right.negated()]); // The result should test `replacement`, and when it's true, it should produce the same // output that input would when `left ∧ right` is true. When replacement is false, it @@ -1723,18 +2224,18 @@ impl<'db> Node<'db> { // false // // (Note that the `else` branch shouldn't be reachable, but we have to provide something!) - let left_node = Node::new_satisfied_constraint(db, left, left_source_order); - let right_node = Node::new_satisfied_constraint(db, right, right_source_order); - let right_result = right_node.ite(db, Node::AlwaysFalse, when_left_but_not_right); - let left_result = left_node.ite(db, right_result, when_not_left); - let result = replacement.ite(db, when_left_and_right, left_result); + let left_node = Node::new_satisfied_constraint(builder, left, left_source_order); + let right_node = Node::new_satisfied_constraint(builder, right, right_source_order); + let right_result = right_node.ite(builder, ALWAYS_FALSE, when_left_but_not_right); + let left_result = left_node.ite(builder, right_result, when_not_left); + let result = replacement.ite(builder, when_left_and_right, left_result); // Lastly, verify that the result is consistent with the input. (It must produce the same // results when `left ∧ right`.) If it doesn't, the substitution isn't valid, and we should // return the original BDD unmodified. - let validity = replacement.iff(db, left_node.and(db, right_node)); - let constrained_original = self.and(db, validity); - let constrained_replacement = result.and(db, validity); + let validity = replacement.iff(builder, left_node.and(builder, right_node)); + let constrained_original = self.and(builder, validity); + let constrained_replacement = result.and(builder, validity); if constrained_original == constrained_replacement { result } else { @@ -1743,14 +2244,16 @@ impl<'db> Node<'db> { } /// Returns a new BDD with any occurrence of `left ∨ right` replaced with `replacement`. - fn substitute_union( + #[expect(clippy::too_many_arguments)] + fn substitute_union<'db>( self, db: &'db dyn Db, - left: ConstraintAssignment<'db>, + builder: &ConstraintSetBuilder<'db>, + left: ConstraintAssignment, left_source_order: usize, - right: ConstraintAssignment<'db>, + right: ConstraintAssignment, right_source_order: usize, - replacement: Node<'db>, + replacement: NodeId, ) -> Self { // We perform a Shannon expansion to find out what the input BDD evaluates to when: // - left and right are both true @@ -1758,7 +2261,7 @@ impl<'db> Node<'db> { // - left is false and right is true // - left and right are both false // This covers the entire truth table of `left ∨ right`. - let (when_l1_r1, both_found) = self.restrict(db, [left, right]); + let (when_l1_r1, both_found) = self.restrict(db, builder, [left, right]); if !both_found { // If left and right are not both present in the input BDD, we should not even attempt // the substitution, since the Shannon expansion might introduce the missing variables! @@ -1766,9 +2269,9 @@ impl<'db> Node<'db> { // with the input. return self; } - let (when_l0_r0, _) = self.restrict(db, [left.negated(), right.negated()]); - let (when_l1_r0, _) = self.restrict(db, [left, right.negated()]); - let (when_l0_r1, _) = self.restrict(db, [left.negated(), right]); + let (when_l0_r0, _) = self.restrict(db, builder, [left.negated(), right.negated()]); + let (when_l1_r0, _) = self.restrict(db, builder, [left, right.negated()]); + let (when_l0_r1, _) = self.restrict(db, builder, [left.negated(), right]); // The result should test `replacement`, and when it's true, it should produce the same // output that input would when `left ∨ right` is true. For OR, this is the union of what @@ -1781,19 +2284,19 @@ impl<'db> Node<'db> { // else // when_l0_r0 let result = replacement.ite( - db, - when_l1_r0.or(db, when_l0_r1.or(db, when_l1_r1)), + builder, + when_l1_r0.or(builder, when_l0_r1.or(builder, when_l1_r1)), when_l0_r0, ); // Lastly, verify that the result is consistent with the input. (It must produce the same // results when `left ∨ right`.) If it doesn't, the substitution isn't valid, and we should // return the original BDD unmodified. - let left_node = Node::new_satisfied_constraint(db, left, left_source_order); - let right_node = Node::new_satisfied_constraint(db, right, right_source_order); - let validity = replacement.iff(db, left_node.or(db, right_node)); - let constrained_original = self.and(db, validity); - let constrained_replacement = result.and(db, validity); + let left_node = Node::new_satisfied_constraint(builder, left, left_source_order); + let right_node = Node::new_satisfied_constraint(builder, right, right_source_order); + let validity = replacement.iff(builder, left_node.or(builder, right_node)); + let constrained_original = self.and(builder, validity); + let constrained_replacement = result.and(builder, validity); if constrained_original == constrained_replacement { result } else { @@ -1807,15 +2310,16 @@ impl<'db> Node<'db> { /// the constraint.) fn for_each_constraint( self, - db: &'db dyn Db, - f: &mut dyn FnMut(ConstrainedTypeVar<'db>, usize), + builder: &ConstraintSetBuilder<'_>, + f: &mut dyn FnMut(ConstraintId, usize), ) { - let Node::Interior(interior) = self else { + if self.is_terminal() { return; - }; - f(interior.constraint(db), interior.source_order(db)); - interior.if_true(db).for_each_constraint(db, f); - interior.if_false(db).for_each_constraint(db, f); + } + let interior = builder.interior_node_data(self); + f(interior.constraint, interior.source_order); + interior.if_true.for_each_constraint(builder, f); + interior.if_false.for_each_constraint(builder, f); } /// Simplifies a BDD, replacing constraints with simpler or smaller constraints where possible. @@ -1839,33 +2343,37 @@ impl<'db> Node<'db> { /// purposes). That means we have some tech debt here, since there is a lot of duplicate logic /// between `simplify_for_display` and `SequentMap`. It would be nice to update our display /// logic to use the sequent map as much as possible. But that can happen later. - fn simplify_for_display(self, db: &'db dyn Db) -> Self { - match self { + fn simplify_for_display<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + ) -> Self { + match self.node() { Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(interior) => interior.simplify(db), + Node::Interior(interior) => interior.simplify(db, builder), } } /// Returns clauses describing all of the variable assignments that cause this BDD to evaluate /// to `true`. (This translates the boolean function that this BDD represents into DNF form.) - fn satisfied_clauses(self, db: &'db dyn Db) -> SatisfiedClauses<'db> { - struct Searcher<'db> { - clauses: SatisfiedClauses<'db>, - current_clause: SatisfiedClause<'db>, + fn satisfied_clauses(self, builder: &ConstraintSetBuilder<'_>) -> SatisfiedClauses { + struct Searcher { + clauses: SatisfiedClauses, + current_clause: SatisfiedClause, } - impl<'db> Searcher<'db> { - fn visit_node(&mut self, db: &'db dyn Db, node: Node<'db>) { - match node { + impl Searcher { + fn visit_node(&mut self, builder: &ConstraintSetBuilder<'_>, node: NodeId) { + match node.node() { Node::AlwaysFalse => {} Node::AlwaysTrue => self.clauses.push(self.current_clause.clone()), - Node::Interior(interior) => { - let interior_constraint = interior.constraint(db); - self.current_clause.push(interior_constraint.when_true()); - self.visit_node(db, interior.if_true(db)); + Node::Interior(_) => { + let interior = builder.interior_node_data(node); + self.current_clause.push(interior.constraint.when_true()); + self.visit_node(builder, interior.if_true); self.current_clause.pop(); - self.current_clause.push(interior_constraint.when_false()); - self.visit_node(db, interior.if_false(db)); + self.current_clause.push(interior.constraint.when_false()); + self.visit_node(builder, interior.if_false); self.current_clause.pop(); } } @@ -1876,36 +2384,41 @@ impl<'db> Node<'db> { clauses: SatisfiedClauses::default(), current_clause: SatisfiedClause::default(), }; - searcher.visit_node(db, self); + searcher.visit_node(builder, self); searcher.clauses } - fn display(self, db: &'db dyn Db) -> impl Display { + fn display<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> impl Display { // To render a BDD in DNF form, you perform a depth-first search of the BDD tree, looking // for any path that leads to the AlwaysTrue terminal. Each such path represents one of the // intersection clauses in the DNF form. The path traverses zero or more interior nodes, // and takes either the true or false edge from each one. That gives you the positive or // negative individual constraints in the path's clause. - struct DisplayNode<'db> { - node: Node<'db>, + struct DisplayNode<'db, 'c> { + node: NodeId, db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, } - impl Display for DisplayNode<'_> { + impl Display for DisplayNode<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.node { + match self.node.node() { Node::AlwaysTrue => f.write_str("always"), Node::AlwaysFalse => f.write_str("never"), Node::Interior(_) => { - let mut clauses = self.node.satisfied_clauses(self.db); - clauses.simplify(self.db); - Display::fmt(&clauses.display(self.db), f) + let mut clauses = self.node.satisfied_clauses(self.builder); + clauses.simplify(self.db, self.builder); + Display::fmt(&clauses.display(self.db, self.builder), f) } } } } - DisplayNode { node: self, db } + DisplayNode { + node: self, + db, + builder, + } } /// Displays the full graph structure of this BDD. `prefix` will be output before each line @@ -1926,42 +2439,51 @@ impl<'db> Node<'db> { /// │ └─₀ never /// └─₀ never /// ``` - fn display_graph(self, db: &'db dyn Db, prefix: &dyn Display) -> impl Display { + fn display_graph<'db, 'a>( + self, + db: &'db dyn Db, + builder: &'a ConstraintSetBuilder<'db>, + prefix: &'a dyn Display, + ) -> impl Display + 'a { struct DisplayNode<'a, 'db> { db: &'db dyn Db, - node: Node<'db>, + builder: &'a ConstraintSetBuilder<'db>, + node: NodeId, prefix: &'a dyn Display, - seen: RefCell>>, + seen: RefCell>, } fn format_node<'db>( db: &'db dyn Db, - node: Node<'db>, + builder: &ConstraintSetBuilder<'db>, + node: NodeId, prefix: &dyn Display, - seen: &RefCell>>, + seen: &RefCell>, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { - match node { + match node.node() { Node::AlwaysTrue => write!(f, "always"), Node::AlwaysFalse => write!(f, "never"), - Node::Interior(interior) => { - let (index, is_new) = seen.borrow_mut().insert_full(interior); + Node::Interior(_) => { + let (index, is_new) = seen.borrow_mut().insert_full(node); if !is_new { return write!(f, "<{index}> SHARED"); } + let interior = builder.interior_node_data(node); write!( f, "<{index}> {} {}/{}", - interior.constraint(db).display(db), - interior.source_order(db), - interior.max_source_order(db), + interior.constraint.display(db, builder), + interior.source_order, + interior.max_source_order, )?; // Calling display_graph recursively here causes rustc to claim that the // expect(unused) up above is unfulfilled! write!(f, "\n{prefix}┡━₁ ",)?; format_node( db, - interior.if_true(db), + builder, + interior.if_true, &format_args!("{prefix}│ ",), seen, f, @@ -1969,7 +2491,8 @@ impl<'db> Node<'db> { write!(f, "\n{prefix}└─₀ ",)?; format_node( db, - interior.if_false(db), + builder, + interior.if_false, &format_args!("{prefix} ",), seen, f, @@ -1981,12 +2504,13 @@ impl<'db> Node<'db> { impl Display for DisplayNode<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - format_node(self.db, self.node, self.prefix, &self.seen, f) + format_node(self.db, self.builder, self.node, self.prefix, &self.seen, f) } } DisplayNode { db, + builder, node: self, prefix, seen: RefCell::default(), @@ -1994,12 +2518,46 @@ impl<'db> Node<'db> { } } +impl Debug for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut f = f.debug_tuple("Node"); + match self.node() { + // We use format_args instead of rendering the strings directly so that we don't get + // any quotes in the output: ScopedReachabilityConstraintId(AlwaysTrue) instead of + // ScopedReachabilityConstraintId("AlwaysTrue"). + Node::AlwaysTrue => f.field(&format_args!("AlwaysTrue")), + Node::AlwaysFalse => f.field(&format_args!("AlwaysFalse")), + Node::Interior(_) => f.field(&self.0), + }; + f.finish() + } +} + +impl Idx for NodeId { + #[inline] + fn new(value: usize) -> Self { + assert!(value <= (SMALLEST_TERMINAL.0 as usize)); + #[expect(clippy::cast_possible_truncation)] + Self(value as u32) + } + + #[inline] + fn index(self) -> usize { + debug_assert!(!self.is_terminal()); + self.0 as usize + } +} + +/// The index of an interior node within a [`ConstraintSetStorage`]. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +struct InteriorNode(NodeId); + /// An interior node of a BDD -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -struct InteriorNode<'db> { - constraint: ConstrainedTypeVar<'db>, - if_true: Node<'db>, - if_false: Node<'db>, +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] +struct InteriorNodeData { + constraint: ConstraintId, + if_true: NodeId, + if_false: NodeId, /// Represents the order in which this node's constraint was added to the containing constraint /// set, relative to all of the other constraints in the set. This starts off at 1 for a simple @@ -2012,130 +2570,207 @@ struct InteriorNode<'db> { max_source_order: usize, } -// The Salsa heap is tracked separately. -impl get_size2::GetSize for InteriorNode<'_> {} +impl InteriorNode { + fn node(self) -> NodeId { + self.0 + } -#[salsa::tracked] -impl<'db> InteriorNode<'db> { - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - fn negate(self, db: &'db dyn Db) -> Node<'db> { - Node::new( - db, - self.constraint(db), - self.if_true(db).negate(db), - self.if_false(db).negate(db), - self.source_order(db), - ) + fn negate(self, builder: &ConstraintSetBuilder<'_>) -> NodeId { + let key = self.node(); + let storage = builder.storage.borrow(); + if let Some(result) = storage.negate_cache.get(&key) { + return *result; + } + drop(storage); + + let interior = builder.interior_node_data(self.node()); + let result = NodeId::new( + builder, + interior.constraint, + interior.if_true.negate(builder), + interior.if_false.negate(builder), + interior.source_order, + ); + + let mut storage = builder.storage.borrow_mut(); + storage.negate_cache.insert(key, result); + result } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - fn or(self, db: &'db dyn Db, other: Self, other_offset: usize) -> Node<'db> { - let self_constraint = self.constraint(db); - let other_constraint = other.constraint(db); - match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) { - Ordering::Equal => Node::new( - db, - self_constraint, - self.if_true(db) - .or_inner(db, other.if_true(db), other_offset), - self.if_false(db) - .or_inner(db, other.if_false(db), other_offset), - self.source_order(db), + fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self, other_offset: usize) -> NodeId { + let key = (self.node(), other.node(), other_offset); + let storage = builder.storage.borrow(); + if let Some(result) = storage.or_cache.get(&key) { + return *result; + } + drop(storage); + + let self_interior = builder.interior_node_data(self.node()); + let self_ordering = self_interior.constraint.ordering(); + let other_interior = builder.interior_node_data(other.node()); + let other_ordering = other_interior.constraint.ordering(); + let result = match self_ordering.cmp(&other_ordering) { + Ordering::Equal => NodeId::new( + builder, + self_interior.constraint, + self_interior + .if_true + .or_inner(builder, other_interior.if_true, other_offset), + self_interior + .if_false + .or_inner(builder, other_interior.if_false, other_offset), + self_interior.source_order, ), - Ordering::Less => Node::new( - db, - self_constraint, - self.if_true(db) - .or_inner(db, Node::Interior(other), other_offset), - self.if_false(db) - .or_inner(db, Node::Interior(other), other_offset), - self.source_order(db), + Ordering::Less => NodeId::new( + builder, + self_interior.constraint, + self_interior + .if_true + .or_inner(builder, other.node(), other_offset), + self_interior + .if_false + .or_inner(builder, other.node(), other_offset), + self_interior.source_order, ), - Ordering::Greater => Node::new( - db, - other_constraint, - Node::Interior(self).or_inner(db, other.if_true(db), other_offset), - Node::Interior(self).or_inner(db, other.if_false(db), other_offset), - other.source_order(db) + other_offset, + Ordering::Greater => NodeId::new( + builder, + other_interior.constraint, + self.node() + .or_inner(builder, other_interior.if_true, other_offset), + self.node() + .or_inner(builder, other_interior.if_false, other_offset), + other_interior.source_order + other_offset, ), - } + }; + + let mut storage = builder.storage.borrow_mut(); + storage.or_cache.insert(key, result); + result } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - fn and(self, db: &'db dyn Db, other: Self, other_offset: usize) -> Node<'db> { - let self_constraint = self.constraint(db); - let other_constraint = other.constraint(db); - match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) { - Ordering::Equal => Node::new( - db, - self_constraint, - self.if_true(db) - .and_inner(db, other.if_true(db), other_offset), - self.if_false(db) - .and_inner(db, other.if_false(db), other_offset), - self.source_order(db), + fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self, other_offset: usize) -> NodeId { + let key = (self.node(), other.node(), other_offset); + let storage = builder.storage.borrow(); + if let Some(result) = storage.and_cache.get(&key) { + return *result; + } + drop(storage); + + let self_interior = builder.interior_node_data(self.node()); + let self_ordering = self_interior.constraint.ordering(); + let other_interior = builder.interior_node_data(other.node()); + let other_ordering = other_interior.constraint.ordering(); + let result = match self_ordering.cmp(&other_ordering) { + Ordering::Equal => NodeId::new( + builder, + self_interior.constraint, + self_interior + .if_true + .and_inner(builder, other_interior.if_true, other_offset), + self_interior + .if_false + .and_inner(builder, other_interior.if_false, other_offset), + self_interior.source_order, ), - Ordering::Less => Node::new( - db, - self_constraint, - self.if_true(db) - .and_inner(db, Node::Interior(other), other_offset), - self.if_false(db) - .and_inner(db, Node::Interior(other), other_offset), - self.source_order(db), + Ordering::Less => NodeId::new( + builder, + self_interior.constraint, + self_interior + .if_true + .and_inner(builder, other.node(), other_offset), + self_interior + .if_false + .and_inner(builder, other.node(), other_offset), + self_interior.source_order, ), - Ordering::Greater => Node::new( - db, - other_constraint, - Node::Interior(self).and_inner(db, other.if_true(db), other_offset), - Node::Interior(self).and_inner(db, other.if_false(db), other_offset), - other.source_order(db) + other_offset, + Ordering::Greater => NodeId::new( + builder, + other_interior.constraint, + self.node() + .and_inner(builder, other_interior.if_true, other_offset), + self.node() + .and_inner(builder, other_interior.if_false, other_offset), + other_interior.source_order + other_offset, ), - } + }; + + let mut storage = builder.storage.borrow_mut(); + storage.and_cache.insert(key, result); + result } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - fn iff(self, db: &'db dyn Db, other: Self, other_offset: usize) -> Node<'db> { - let self_constraint = self.constraint(db); - let other_constraint = other.constraint(db); - match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) { - Ordering::Equal => Node::new( - db, - self_constraint, - self.if_true(db) - .iff_inner(db, other.if_true(db), other_offset), - self.if_false(db) - .iff_inner(db, other.if_false(db), other_offset), - self.source_order(db), + fn iff(self, builder: &ConstraintSetBuilder<'_>, other: Self, other_offset: usize) -> NodeId { + let key = (self.node(), other.node(), other_offset); + let storage = builder.storage.borrow(); + if let Some(result) = storage.iff_cache.get(&key) { + return *result; + } + drop(storage); + + let self_interior = builder.interior_node_data(self.node()); + let self_ordering = self_interior.constraint.ordering(); + let other_interior = builder.interior_node_data(other.node()); + let other_ordering = other_interior.constraint.ordering(); + let result = match self_ordering.cmp(&other_ordering) { + Ordering::Equal => NodeId::new( + builder, + self_interior.constraint, + self_interior + .if_true + .iff_inner(builder, other_interior.if_true, other_offset), + self_interior + .if_false + .iff_inner(builder, other_interior.if_false, other_offset), + self_interior.source_order, ), - Ordering::Less => Node::new( - db, - self_constraint, - self.if_true(db) - .iff_inner(db, Node::Interior(other), other_offset), - self.if_false(db) - .iff_inner(db, Node::Interior(other), other_offset), - self.source_order(db), + Ordering::Less => NodeId::new( + builder, + self_interior.constraint, + self_interior + .if_true + .iff_inner(builder, other.node(), other_offset), + self_interior + .if_false + .iff_inner(builder, other.node(), other_offset), + self_interior.source_order, ), - Ordering::Greater => Node::new( - db, - other_constraint, - Node::Interior(self).iff_inner(db, other.if_true(db), other_offset), - Node::Interior(self).iff_inner(db, other.if_false(db), other_offset), - other.source_order(db) + other_offset, + Ordering::Greater => NodeId::new( + builder, + other_interior.constraint, + self.node() + .iff_inner(builder, other_interior.if_true, other_offset), + self.node() + .iff_inner(builder, other_interior.if_false, other_offset), + other_interior.source_order + other_offset, ), - } + }; + + let mut storage = builder.storage.borrow_mut(); + storage.iff_cache.insert(key, result); + result } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - fn exists_one(self, db: &'db dyn Db, bound_typevar: BoundTypeVarIdentity<'db>) -> Node<'db> { - let mut path = self.path_assignments(db); + fn exists_one<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + bound_typevar: BoundTypeVarIdentity<'db>, + ) -> NodeId { + let key = (self.node(), bound_typevar); + let storage = builder.storage.borrow(); + if let Some(result) = storage.exists_one_cache.get(&key) { + return *result; + } + drop(storage); + + let mut path = self.path_assignments(builder); let mentions_typevar = |ty: Type<'db>| match ty { Type::TypeVar(haystack) => haystack.identity(db) == bound_typevar, _ => false, }; - self.abstract_one_inner( + let result = self.abstract_one_inner( db, + builder, // Remove any node that constrains `bound_typevar`, or that has a lower/upper bound // that mentions `bound_typevar`. // TODO: This will currently remove constraints that mention a typevar, but the sequent @@ -2144,30 +2779,35 @@ impl<'db> InteriorNode<'db> { // But that requires `T ≤ int ∧ U ≤ Sequence[T] → U ≤ Sequence[int]` to exist in the // sequent map. It doesn't, and so we currently produce `U ≤ Unknown` in this case. &mut |constraint| { - if constraint.typevar(db).identity(db) == bound_typevar { + let constraint = builder.constraint_data(constraint); + if constraint.typevar.identity(db) == bound_typevar { return true; } - if any_over_type(db, constraint.lower(db), false, mentions_typevar) { + if any_over_type(db, constraint.lower, false, mentions_typevar) { return true; } - if any_over_type(db, constraint.upper(db), false, mentions_typevar) { + if any_over_type(db, constraint.upper, false, mentions_typevar) { return true; } false }, &mut path, - ) + ); + + let mut storage = builder.storage.borrow_mut(); + storage.exists_one_cache.insert(key, result); + result } - fn abstract_one_inner( + fn abstract_one_inner<'db>( self, db: &'db dyn Db, - should_remove: &mut dyn FnMut(ConstrainedTypeVar<'db>) -> bool, - path: &mut PathAssignments<'db>, - ) -> Node<'db> { - let self_constraint = self.constraint(db); - let self_source_order = self.source_order(db); - if should_remove(self_constraint) { + builder: &ConstraintSetBuilder<'db>, + should_remove: &mut dyn FnMut(ConstraintId) -> bool, + path: &mut PathAssignments, + ) -> NodeId { + let self_interior = builder.interior_node_data(self.node()); + if should_remove(self_interior.constraint) { // If we should remove constraints involving this typevar, then we replace this node // with the OR of its if_false/if_true edges. That is, the result is true if there's // any assignment of this node's constraint that is true. @@ -2180,14 +2820,19 @@ impl<'db> InteriorNode<'db> { // TODO: This might not be stable enough, if we add more than one derived fact for this // constraint. If we still see inconsistent test output, we might need a more complex // way of tracking source order for derived facts. - let self_source_order = self.source_order(db); let if_true = path .walk_edge( db, - self_constraint.when_true(), - self_source_order, + builder, + self_interior.constraint.when_true(), + self_interior.source_order, |path, new_range| { - let branch = self.if_true(db).abstract_one_inner(db, should_remove, path); + let branch = self_interior.if_true.abstract_one_inner( + db, + builder, + should_remove, + path, + ); path.assignments[new_range] .iter() .filter(|(assignment, _)| { @@ -2197,22 +2842,30 @@ impl<'db> InteriorNode<'db> { }) .fold(branch, |branch, (assignment, source_order)| { branch.and( - db, - Node::new_satisfied_constraint(db, *assignment, *source_order), + builder, + Node::new_satisfied_constraint( + builder, + *assignment, + *source_order, + ), ) }) }, ) - .unwrap_or(Node::AlwaysFalse); + .unwrap_or(ALWAYS_FALSE); let if_false = path .walk_edge( db, - self_constraint.when_false(), - self_source_order, + builder, + self_interior.constraint.when_false(), + self_interior.source_order, |path, new_range| { - let branch = self - .if_false(db) - .abstract_one_inner(db, should_remove, path); + let branch = self_interior.if_false.abstract_one_inner( + db, + builder, + should_remove, + path, + ); path.assignments[new_range] .iter() .filter(|(assignment, _)| { @@ -2222,80 +2875,113 @@ impl<'db> InteriorNode<'db> { }) .fold(branch, |branch, (assignment, source_order)| { branch.and( - db, - Node::new_satisfied_constraint(db, *assignment, *source_order), + builder, + Node::new_satisfied_constraint( + builder, + *assignment, + *source_order, + ), ) }) }, ) - .unwrap_or(Node::AlwaysFalse); - if_true.or(db, if_false) + .unwrap_or(ALWAYS_FALSE); + if_true.or(builder, if_false) } else { // Otherwise, we abstract the if_false/if_true edges recursively. let if_true = path .walk_edge( db, - self_constraint.when_true(), - self_source_order, - |path, _| self.if_true(db).abstract_one_inner(db, should_remove, path), + builder, + self_interior.constraint.when_true(), + self_interior.source_order, + |path, _| { + self_interior + .if_true + .abstract_one_inner(db, builder, should_remove, path) + }, ) - .unwrap_or(Node::AlwaysFalse); + .unwrap_or(ALWAYS_FALSE); let if_false = path .walk_edge( db, - self_constraint.when_false(), - self_source_order, + builder, + self_interior.constraint.when_false(), + self_interior.source_order, |path, _| { - self.if_false(db) - .abstract_one_inner(db, should_remove, path) + self_interior + .if_false + .abstract_one_inner(db, builder, should_remove, path) }, ) - .unwrap_or(Node::AlwaysFalse); + .unwrap_or(ALWAYS_FALSE); // NB: We cannot use `Node::new` here, because the recursive calls might introduce new // derived constraints into the result, and those constraints might appear before this // one in the BDD ordering. - Node::new_constraint(db, self_constraint, self.source_order(db)) - .ite(db, if_true, if_false) + Node::new_constraint( + builder, + self_interior.constraint, + self_interior.source_order, + ) + .ite(builder, if_true, if_false) } } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - fn restrict_one( + fn restrict_one<'db>( self, db: &'db dyn Db, - assignment: ConstraintAssignment<'db>, - ) -> (Node<'db>, bool) { - // If this node's variable is larger than the assignment's variable, then we have reached a - // point in the BDD where the assignment can no longer affect the result, - // and we can return early. - let self_constraint = self.constraint(db); - if assignment.constraint().ordering(db) < self_constraint.ordering(db) { - return (Node::Interior(self), false); - } - - // Otherwise, check if this node's variable is in the assignment. If so, substitute the - // variable by replacing this node with its if_false/if_true edge, accordingly. - if assignment == self_constraint.when_true() { - (self.if_true(db), true) - } else if assignment == self_constraint.when_false() { - (self.if_false(db), true) - } else { - let (if_true, found_in_true) = self.if_true(db).restrict_one(db, assignment); - let (if_false, found_in_false) = self.if_false(db).restrict_one(db, assignment); - ( - Node::new( - db, - self_constraint, - if_true, - if_false, - self.source_order(db), - ), - found_in_true || found_in_false, - ) + builder: &ConstraintSetBuilder<'db>, + assignment: ConstraintAssignment, + ) -> (NodeId, bool) { + let key = (self.node(), assignment); + let storage = builder.storage.borrow(); + if let Some(result) = storage.restrict_one_cache.get(&key) { + return *result; } + drop(storage); + + let self_interior = builder.interior_node_data(self.node()); + let self_ordering = self_interior.constraint.ordering(); + let result = if assignment.constraint().ordering() < self_ordering { + // If this node's variable is larger than the assignment's variable, then we have reached a + // point in the BDD where the assignment can no longer affect the result, + // and we can return early. + (self.node(), false) + } else { + // Otherwise, check if this node's variable is in the assignment. If so, substitute the + // variable by replacing this node with its if_false/if_true edge, accordingly. + if assignment == self_interior.constraint.when_true() { + (self_interior.if_true, true) + } else if assignment == self_interior.constraint.when_false() { + (self_interior.if_false, true) + } else { + let (if_true, found_in_true) = + self_interior.if_true.restrict_one(db, builder, assignment); + let (if_false, found_in_false) = + self_interior.if_false.restrict_one(db, builder, assignment); + ( + NodeId::new( + builder, + self_interior.constraint, + if_true, + if_false, + self_interior.source_order, + ), + found_in_true || found_in_false, + ) + } + }; + + let mut storage = builder.storage.borrow_mut(); + storage.restrict_one_cache.insert(key, result); + result } - fn solutions(self, db: &'db dyn Db) -> Solutions<'db> { + fn solutions<'db, 'c>( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + ) -> Solutions<'db, 'c> { #[derive(Default)] struct Bounds<'db> { lower: FxIndexSet>, @@ -2334,18 +3020,26 @@ impl<'db> InteriorNode<'db> { } } - #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] - fn solutions_inner<'db>( + fn solutions_inner<'db, 'c>( db: &'db dyn Db, - interior: InteriorNode<'db>, - ) -> Vec> { + builder: &'c ConstraintSetBuilder<'db>, + interior: NodeId, + ) -> Ref<'c, Vec>> { + let key = interior; + let storage = builder.storage.borrow(); + if let Ok(solutions) = + Ref::filter_map(storage, |storage| storage.solutions_cache.get(&key)) + { + return solutions; + } + // Sort the constraints in each path by their `source_order`s, to ensure that we construct // any unions or intersections in our type mappings in a stable order. Constraints might // come out of `PathAssignment`s with identical `source_order`s, but if they do, those // "tied" constraints will still be ordered in a stable way. So we need a stable sort to // retain that stable per-tie ordering. let mut sorted_paths = Vec::new(); - Node::Interior(interior).for_each_path(db, |path| { + interior.for_each_path(db, builder, |path| { let mut path: Vec<_> = path.positive_constraints().collect(); path.sort_by_key(|(_, source_order)| *source_order); sorted_paths.push(path); @@ -2362,9 +3056,10 @@ impl<'db> InteriorNode<'db> { 'paths: for path in sorted_paths { mappings.clear(); for (constraint, _) in path { - let typevar = constraint.typevar(db); - let lower = constraint.lower(db); - let upper = constraint.upper(db); + let constraint = builder.constraint_data(constraint); + let typevar = constraint.typevar; + let lower = constraint.lower; + let upper = constraint.upper; let bounds = mappings.entry(typevar).or_default(); bounds.add_lower(db, lower); bounds.add_upper(db, upper); @@ -2455,25 +3150,31 @@ impl<'db> InteriorNode<'db> { solutions.push(solution); } - solutions + let mut storage = builder.storage.borrow_mut(); + storage.solutions_cache.insert(key, solutions); + drop(storage); + + let storage = builder.storage.borrow(); + Ref::map(storage, |storage| &storage.solutions_cache[&key]) } - let solutions = solutions_inner(db, self); + let solutions = solutions_inner(db, builder, self.node()); if solutions.is_empty() { return Solutions::Unsatisfiable; } Solutions::Constrained(solutions) } - fn path_assignments(self, db: &'db dyn Db) -> PathAssignments<'db> { + fn path_assignments(self, builder: &ConstraintSetBuilder<'_>) -> PathAssignments { // Sort the constraints in this BDD by their `source_order`s before adding them to the // sequent map. This ensures that constraints appear in the sequent map in a stable order. // The constraints mentioned in a BDD should all have distinct `source_order`s, so an // unstable sort is fine. let mut constraints: SmallVec<[_; 8]> = SmallVec::new(); - Node::Interior(self).for_each_constraint(db, &mut |constraint, source_order| { - constraints.push((constraint, source_order)); - }); + self.node() + .for_each_constraint(builder, &mut |constraint, source_order| { + constraints.push((constraint, source_order)); + }); constraints.sort_unstable_by_key(|(_, source_order)| *source_order); PathAssignments::new(constraints.into_iter().map(|(constraint, _)| constraint)) @@ -2484,8 +3185,14 @@ impl<'db> InteriorNode<'db> { /// This is calculated by looking at the relationships that exist between the constraints that /// are mentioned in the BDD. For instance, if one constraint implies another (`x → y`), then /// `x ∧ ¬y` is not a valid input, and we can rewrite any occurrences of `x ∨ y` into `y`. - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - fn simplify(self, db: &'db dyn Db) -> Node<'db> { + fn simplify<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { + let key = self.node(); + let storage = builder.storage.borrow(); + if let Some(result) = storage.simplify_cache.get(&key) { + return *result; + } + drop(storage); + // To simplify a non-terminal BDD, we find all pairs of constraints that are mentioned in // the BDD. If any of those pairs can be simplified to some other BDD, we perform a // substitution to replace the pair with the simplification. @@ -2503,10 +3210,11 @@ impl<'db> InteriorNode<'db> { // need to compare a constraint against itself, and because ordering doesn't matter.) let mut seen_constraints = FxHashSet::default(); let mut source_orders = FxHashMap::default(); - Node::Interior(self).for_each_constraint(db, &mut |constraint, source_order| { - seen_constraints.insert(constraint); - source_orders.insert(constraint, source_order); - }); + self.node() + .for_each_constraint(builder, &mut |constraint, source_order| { + seen_constraints.insert(constraint); + source_orders.insert(constraint, source_order); + }); let mut to_visit: Vec<(_, _)> = (seen_constraints.iter().copied()) .tuple_combinations() .collect(); @@ -2516,92 +3224,89 @@ impl<'db> InteriorNode<'db> { // source order. (We do not have any test cases that depend on constraint sets being // displayed in a consistent ordering, so we don't need to be clever in assigning these // `source_order`s.) - let mut simplified = Node::Interior(self); - let mut next_source_order = self.max_source_order(db) + 1; + let mut simplified = self.node(); + let self_interior = builder.interior_node_data(self.node()); + let mut next_source_order = self_interior.max_source_order + 1; while let Some((left_constraint, right_constraint)) = to_visit.pop() { let left_source_order = source_orders[&left_constraint]; let right_source_order = source_orders[&right_constraint]; // If the constraints refer to different typevars, the only simplifications we can make // are of the form `S ≤ T ∧ T ≤ int → S ≤ int`. - let left_typevar = left_constraint.typevar(db); - let right_typevar = right_constraint.typevar(db); + let left_constraint_data = builder.constraint_data(left_constraint); + let left_typevar = left_constraint_data.typevar; + let right_constraint_data = builder.constraint_data(right_constraint); + let right_typevar = right_constraint_data.typevar; if !left_typevar.is_same_typevar_as(db, right_typevar) { // We've structured our constraints so that a typevar's upper/lower bound can only // be another typevar if the bound is "later" in our arbitrary ordering. That means // we only have to check this pair of constraints in one direction — though we do // have to figure out which of the two typevars is constrained, and which one is // the upper/lower bound. - let (bound_typevar, bound_constraint, constrained_typevar, constrained_constraint) = - if left_typevar.can_be_bound_for(db, right_typevar) { - ( - left_typevar, - left_constraint, - right_typevar, - right_constraint, - ) + let (bound_constraint, constrained_constraint) = + if left_typevar.can_be_bound_for(db, builder, right_typevar) { + (left_constraint, right_constraint) } else { - ( - right_typevar, - right_constraint, - left_typevar, - left_constraint, - ) + (right_constraint, left_constraint) }; + let bound_constraint_data = builder.constraint_data(bound_constraint); + let bound_typevar = bound_constraint_data.typevar; + let constrained_constraint_data = builder.constraint_data(constrained_constraint); + let constrained_typevar = constrained_constraint_data.typevar; // We then look for cases where the "constrained" typevar's upper and/or lower // bound matches the "bound" typevar. If so, we're going to add an implication to // the constraint set that replaces the upper/lower bound that matched with the // bound constraint's corresponding bound. let (new_lower, new_upper) = match ( - constrained_constraint.lower(db), - constrained_constraint.upper(db), + constrained_constraint_data.lower, + constrained_constraint_data.upper, ) { // (B ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ BU) (Type::TypeVar(constrained_lower), Type::TypeVar(constrained_upper)) if constrained_lower.is_same_typevar_as(db, bound_typevar) && constrained_upper.is_same_typevar_as(db, bound_typevar) => { - (bound_constraint.lower(db), bound_constraint.upper(db)) + (bound_constraint_data.lower, bound_constraint_data.upper) } // (CL ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (CL ≤ C ≤ BU) (constrained_lower, Type::TypeVar(constrained_upper)) if constrained_upper.is_same_typevar_as(db, bound_typevar) => { - (constrained_lower, bound_constraint.upper(db)) + (constrained_lower, bound_constraint_data.upper) } // (B ≤ C ≤ CU) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ CU) (Type::TypeVar(constrained_lower), constrained_upper) if constrained_lower.is_same_typevar_as(db, bound_typevar) => { - (bound_constraint.lower(db), constrained_upper) + (bound_constraint_data.lower, constrained_upper) } _ => continue, }; let new_constraint = - ConstrainedTypeVar::new(db, constrained_typevar, new_lower, new_upper); + ConstraintId::new(db, builder, constrained_typevar, new_lower, new_upper); if seen_constraints.contains(&new_constraint) { continue; } - let new_node = Node::new_constraint(db, new_constraint, next_source_order); + let new_node = Node::new_constraint(builder, new_constraint, next_source_order); next_source_order += 1; let positive_left_node = Node::new_satisfied_constraint( - db, + builder, left_constraint.when_true(), left_source_order, ); let positive_right_node = Node::new_satisfied_constraint( - db, + builder, right_constraint.when_true(), right_source_order, ); - let lhs = positive_left_node.and(db, positive_right_node); - let intersection = new_node.ite(db, lhs, Node::AlwaysFalse); - simplified = simplified.and(db, intersection); + let lhs = positive_left_node.and(builder, positive_right_node); + let intersection = new_node.ite(builder, lhs, ALWAYS_FALSE); + simplified = simplified.and(builder, intersection); continue; } @@ -2610,24 +3315,24 @@ impl<'db> InteriorNode<'db> { // other typevars, producing constraints on this typevar that have concrete lower/upper // bounds. That means we can skip the simplifications below if any bound is another // typevar. - if left_constraint.lower(db).is_type_var() - || left_constraint.upper(db).is_type_var() - || right_constraint.lower(db).is_type_var() - || right_constraint.upper(db).is_type_var() + if left_constraint_data.lower.is_type_var() + || left_constraint_data.upper.is_type_var() + || right_constraint_data.lower.is_type_var() + || right_constraint_data.upper.is_type_var() { continue; } // Containment: The range of one constraint might completely contain the range of the // other. If so, there are several potential simplifications. - let larger_smaller = if left_constraint.implies(db, right_constraint) { + let larger_smaller = if left_constraint.implies(db, builder, right_constraint) { Some(( right_constraint, right_source_order, left_constraint, left_source_order, )) - } else if right_constraint.implies(db, left_constraint) { + } else if right_constraint.implies(db, builder, left_constraint) { Some(( left_constraint, left_source_order, @@ -2645,12 +3350,12 @@ impl<'db> InteriorNode<'db> { )) = larger_smaller { let positive_larger_node = Node::new_satisfied_constraint( - db, + builder, larger_constraint.when_true(), larger_source_order, ); let negative_larger_node = Node::new_satisfied_constraint( - db, + builder, larger_constraint.when_false(), larger_source_order, ); @@ -2658,6 +3363,7 @@ impl<'db> InteriorNode<'db> { // larger ∨ smaller = larger simplified = simplified.substitute_union( db, + builder, larger_constraint.when_true(), larger_source_order, smaller_constraint.when_true(), @@ -2668,6 +3374,7 @@ impl<'db> InteriorNode<'db> { // ¬larger ∧ ¬smaller = ¬larger simplified = simplified.substitute_intersection( db, + builder, larger_constraint.when_false(), larger_source_order, smaller_constraint.when_false(), @@ -2679,30 +3386,35 @@ impl<'db> InteriorNode<'db> { // (¬larger removes everything that's present in smaller) simplified = simplified.substitute_intersection( db, + builder, larger_constraint.when_false(), larger_source_order, smaller_constraint.when_true(), smaller_source_order, - Node::AlwaysFalse, + ALWAYS_FALSE, ); // larger ∨ ¬smaller = true // (larger fills in everything that's missing in ¬smaller) simplified = simplified.substitute_union( db, + builder, larger_constraint.when_true(), larger_source_order, smaller_constraint.when_false(), smaller_source_order, - Node::AlwaysTrue, + ALWAYS_TRUE, ); } // There are some simplifications we can make when the intersection of the two // constraints is empty, and others that we can make when the intersection is // non-empty. - match left_constraint.intersect(db, right_constraint) { - IntersectionResult::Simplified(intersection_constraint) => { + match left_constraint.intersect(db, builder, right_constraint) { + IntersectionResult::Simplified(intersection_constraint_data) => { + let intersection_constraint = + builder.intern_constraint(db, intersection_constraint_data); + // If the intersection is non-empty, we need to create a new constraint to // represent that intersection. We also need to add the new constraint to our // seen set and (if we haven't already seen it) to the to-visit queue. @@ -2715,35 +3427,35 @@ impl<'db> InteriorNode<'db> { ); } let positive_intersection_node = Node::new_satisfied_constraint( - db, + builder, intersection_constraint.when_true(), next_source_order, ); let negative_intersection_node = Node::new_satisfied_constraint( - db, + builder, intersection_constraint.when_false(), next_source_order, ); next_source_order += 1; let positive_left_node = Node::new_satisfied_constraint( - db, + builder, left_constraint.when_true(), left_source_order, ); let negative_left_node = Node::new_satisfied_constraint( - db, + builder, left_constraint.when_false(), left_source_order, ); let positive_right_node = Node::new_satisfied_constraint( - db, + builder, right_constraint.when_true(), right_source_order, ); let negative_right_node = Node::new_satisfied_constraint( - db, + builder, right_constraint.when_false(), right_source_order, ); @@ -2751,6 +3463,7 @@ impl<'db> InteriorNode<'db> { // left ∧ right = intersection simplified = simplified.substitute_intersection( db, + builder, left_constraint.when_true(), left_source_order, right_constraint.when_true(), @@ -2761,6 +3474,7 @@ impl<'db> InteriorNode<'db> { // ¬left ∨ ¬right = ¬intersection simplified = simplified.substitute_union( db, + builder, left_constraint.when_false(), left_source_order, right_constraint.when_false(), @@ -2773,22 +3487,24 @@ impl<'db> InteriorNode<'db> { // something from positive constraint) simplified = simplified.substitute_intersection( db, + builder, left_constraint.when_true(), left_source_order, right_constraint.when_false(), right_source_order, - positive_left_node.and(db, negative_intersection_node), + positive_left_node.and(builder, negative_intersection_node), ); // ¬left ∧ right = ¬intersection ∧ right // (save as above but reversed) simplified = simplified.substitute_intersection( db, + builder, left_constraint.when_false(), left_source_order, right_constraint.when_true(), right_source_order, - positive_right_node.and(db, negative_intersection_node), + positive_right_node.and(builder, negative_intersection_node), ); // left ∨ ¬right = intersection ∨ ¬right @@ -2796,22 +3512,24 @@ impl<'db> InteriorNode<'db> { // something to the negative constraint) simplified = simplified.substitute_union( db, + builder, left_constraint.when_true(), left_source_order, right_constraint.when_false(), right_source_order, - negative_right_node.or(db, positive_intersection_node), + negative_right_node.or(builder, positive_intersection_node), ); // ¬left ∨ right = ¬left ∨ intersection // (save as above but reversed) simplified = simplified.substitute_union( db, + builder, left_constraint.when_false(), left_source_order, right_constraint.when_true(), right_source_order, - negative_left_node.or(db, positive_intersection_node), + negative_left_node.or(builder, positive_intersection_node), ); } @@ -2824,12 +3542,12 @@ impl<'db> InteriorNode<'db> { // and right is empty. let positive_left_node = Node::new_satisfied_constraint( - db, + builder, left_constraint.when_true(), left_source_order, ); let positive_right_node = Node::new_satisfied_constraint( - db, + builder, right_constraint.when_true(), right_source_order, ); @@ -2837,27 +3555,30 @@ impl<'db> InteriorNode<'db> { // left ∧ right = false simplified = simplified.substitute_intersection( db, + builder, left_constraint.when_true(), left_source_order, right_constraint.when_true(), right_source_order, - Node::AlwaysFalse, + ALWAYS_FALSE, ); // ¬left ∨ ¬right = true simplified = simplified.substitute_union( db, + builder, left_constraint.when_false(), left_source_order, right_constraint.when_false(), right_source_order, - Node::AlwaysTrue, + ALWAYS_TRUE, ); // left ∧ ¬right = left // (there is nothing in the hole of ¬right that overlaps with left) simplified = simplified.substitute_intersection( db, + builder, left_constraint.when_true(), left_source_order, right_constraint.when_false(), @@ -2869,6 +3590,7 @@ impl<'db> InteriorNode<'db> { // (save as above but reversed) simplified = simplified.substitute_intersection( db, + builder, left_constraint.when_false(), left_source_order, right_constraint.when_true(), @@ -2879,20 +3601,22 @@ impl<'db> InteriorNode<'db> { } } + let mut storage = builder.storage.borrow_mut(); + storage.simplify_cache.insert(key, simplified); simplified } } -#[derive(Clone, Debug)] -pub(crate) enum Solutions<'db> { +#[derive(Debug)] +pub(crate) enum Solutions<'db, 'c> { Unsatisfiable, Unconstrained, - Constrained(&'db Vec>), + Constrained(Ref<'c, Vec>>), } pub(crate) type Solution<'db> = Vec>; -#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update)] +#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] pub(crate) struct TypeVarSolution<'db> { pub(crate) bound_typevar: BoundTypeVarInstance<'db>, pub(crate) solution: Type<'db>, @@ -2900,14 +3624,14 @@ pub(crate) struct TypeVarSolution<'db> { /// An assignment of one BDD variable to either `true` or `false`. (When evaluating a BDD, we /// must provide an assignment for each variable present in the BDD.) -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, salsa::Update)] -pub(crate) enum ConstraintAssignment<'db> { - Positive(ConstrainedTypeVar<'db>), - Negative(ConstrainedTypeVar<'db>), +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +pub(crate) enum ConstraintAssignment { + Positive(ConstraintId), + Negative(ConstraintId), } -impl<'db> ConstraintAssignment<'db> { - fn constraint(self) -> ConstrainedTypeVar<'db> { +impl ConstraintAssignment { + fn constraint(self) -> ConstraintId { match self { ConstraintAssignment::Positive(constraint) => constraint, ConstraintAssignment::Negative(constraint) => constraint, @@ -2934,7 +3658,12 @@ impl<'db> ConstraintAssignment<'db> { /// /// This is used to simplify how we display constraint sets, by removing redundant constraints /// from a clause. - fn implies(self, db: &'db dyn Db, other: Self) -> bool { + fn implies<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + other: Self, + ) -> bool { match (self, other) { // For two positive constraints, one range has to fully contain the other; the smaller // constraint implies the larger. @@ -2944,7 +3673,7 @@ impl<'db> ConstraintAssignment<'db> { ( ConstraintAssignment::Positive(self_constraint), ConstraintAssignment::Positive(other_constraint), - ) => self_constraint.implies(db, other_constraint), + ) => self_constraint.implies(db, builder, other_constraint), // For two negative constraints, one range has to fully contain the other; the ranges // represent "holes", though, so the constraint with the larger range implies the one @@ -2955,7 +3684,7 @@ impl<'db> ConstraintAssignment<'db> { ( ConstraintAssignment::Negative(self_constraint), ConstraintAssignment::Negative(other_constraint), - ) => other_constraint.implies(db, self_constraint), + ) => other_constraint.implies(db, builder, self_constraint), // For a positive and negative constraint, the ranges have to be disjoint, and the // positive range implies the negative range. @@ -2966,7 +3695,7 @@ impl<'db> ConstraintAssignment<'db> { ConstraintAssignment::Positive(self_constraint), ConstraintAssignment::Negative(other_constraint), ) => self_constraint - .intersect(db, other_constraint) + .intersect(db, builder, other_constraint) .is_disjoint(), // It's theoretically possible for a negative constraint to imply a positive constraint @@ -2980,20 +3709,21 @@ impl<'db> ConstraintAssignment<'db> { } } - fn display(self, db: &'db dyn Db) -> impl Display { - struct DisplayConstraintAssignment<'db> { - constraint: ConstraintAssignment<'db>, + fn display<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> impl Display { + struct DisplayConstraintAssignment<'db, 'c> { + constraint: ConstraintAssignment, db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, } - impl Display for DisplayConstraintAssignment<'_> { + impl Display for DisplayConstraintAssignment<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.constraint { ConstraintAssignment::Positive(constraint) => { - constraint.display(self.db).fmt(f) + constraint.display(self.db, self.builder).fmt(f) } ConstraintAssignment::Negative(constraint) => { - constraint.display_negated(self.db).fmt(f) + constraint.display_negated(self.db, self.builder).fmt(f) } } } @@ -3002,6 +3732,7 @@ impl<'db> ConstraintAssignment<'db> { DisplayConstraintAssignment { constraint: self, db, + builder, } } } @@ -3039,42 +3770,48 @@ impl<'db> ConstraintAssignment<'db> { /// new constraint, and then merges those cached sequents into its own sequent map. (That means we /// also share the work of calculating the sequent map across `PathAssignments` for _different_ /// constraint sets.) -#[derive(Clone, Debug, Default, Eq, PartialEq, get_size2::GetSize, salsa::Update)] -struct SequentMap<'db> { +#[derive(Clone, Debug, Default, Eq, PartialEq, get_size2::GetSize)] +struct SequentMap { /// Sequents of the form `¬C₁ → false` - single_tautologies: FxHashSet>, + single_tautologies: FxHashSet, /// Sequents of the form `C₁ ∧ C₂ → false` - pair_impossibilities: FxHashSet<(ConstrainedTypeVar<'db>, ConstrainedTypeVar<'db>)>, + pair_impossibilities: FxHashSet<(ConstraintId, ConstraintId)>, /// Sequents of the form `C₁ ∧ C₂ → D` - pair_implications: FxHashMap< - (ConstrainedTypeVar<'db>, ConstrainedTypeVar<'db>), - FxOrderSet>, - >, + pair_implications: FxIndexMap<(ConstraintId, ConstraintId), FxIndexSet>, /// Sequents of the form `C → D` - single_implications: FxHashMap, FxOrderSet>>, + single_implications: FxIndexMap>, } -impl<'db> SequentMap<'db> { +impl SequentMap { /// Returns a sequent map containing the sequents that we can infer from a single constraint in /// isolation. This method is salsa-tracked so that we only perform this work once per /// constraint. - fn for_constraint(db: &'db dyn Db, constraint: ConstrainedTypeVar<'db>) -> &'db Self { - #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] - fn for_constraint_inner<'db>( - db: &'db dyn Db, - constraint: ConstrainedTypeVar<'db>, - ) -> SequentMap<'db> { - tracing::trace!( - target: "ty_python_semantic::types::constraints::SequentMap", - constraint = %constraint.display(db), - "add sequents for constraint", - ); - let mut map = SequentMap::default(); - map.add_sequents_for_single(db, constraint); - map + fn for_constraint<'db, 'c>( + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + constraint: ConstraintId, + ) -> Ref<'c, Self> { + let key = constraint; + let storage = builder.storage.borrow(); + if let Ok(map) = Ref::filter_map(storage, |storage| storage.single_sequent_cache.get(&key)) + { + return map; } - for_constraint_inner(db, constraint) + tracing::trace!( + target: "ty_python_semantic::types::constraints::SequentMap", + constraint = %constraint.display(db, builder), + "add sequents for constraint", + ); + let mut map = SequentMap::default(); + map.add_sequents_for_single(db, builder, constraint); + + let mut storage = builder.storage.borrow_mut(); + storage.single_sequent_cache.insert(key, map); + drop(storage); + + let storage = builder.storage.borrow(); + Ref::map(storage, |storage| &storage.single_sequent_cache[&key]) } /// Returns a sequent map containing the sequents that we can infer from a pair of constraints. @@ -3083,39 +3820,43 @@ impl<'db> SequentMap<'db> { /// (Note that this method is _not_ commutative; you should provide `left` and `right` in the /// order that they appear in the source code, so that we can construct derived constraints /// that retain that ordering.) - fn for_constraint_pair( + fn for_constraint_pair<'db, 'c>( db: &'db dyn Db, - left: ConstrainedTypeVar<'db>, - right: ConstrainedTypeVar<'db>, - ) -> &'db Self { - #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] - fn for_constraint_pair_inner<'db>( - db: &'db dyn Db, - left: ConstrainedTypeVar<'db>, - right: ConstrainedTypeVar<'db>, - ) -> SequentMap<'db> { - tracing::trace!( - target: "ty_python_semantic::types::constraints::SequentMap", - left = %left.display(db), - right = %right.display(db), - "add sequents for constraint pair", - ); - let mut map = SequentMap::default(); - map.add_sequents_for_pair(db, left, right); - map + builder: &'c ConstraintSetBuilder<'db>, + left: ConstraintId, + right: ConstraintId, + ) -> Ref<'c, Self> { + let key = (left, right); + let storage = builder.storage.borrow(); + if let Ok(map) = Ref::filter_map(storage, |storage| storage.pair_sequent_cache.get(&key)) { + return map; } - for_constraint_pair_inner(db, left, right) + tracing::trace!( + target: "ty_python_semantic::types::constraints::SequentMap", + left = %left.display(db, builder), + right = %right.display(db, builder), + "add sequents for constraint pair", + ); + let mut map = SequentMap::default(); + map.add_sequents_for_pair(db, builder, left, right); + + let mut storage = builder.storage.borrow_mut(); + storage.pair_sequent_cache.insert(key, map); + drop(storage); + + let storage = builder.storage.borrow(); + Ref::map(storage, |storage| &storage.pair_sequent_cache[&key]) } /// Merges the sequents from another sequent map into this one. - fn merge(&mut self, db: &'db dyn Db, other: &Self) { + fn merge(&mut self, other: &Self) { self.single_tautologies.extend(&other.single_tautologies); self.pair_impossibilities .extend(&other.pair_impossibilities); for ((ante1, ante2), post) in &other.pair_implications { self.pair_implications - .entry(Self::pair_key(db, *ante1, *ante2)) + .entry(Self::pair_key(*ante1, *ante2)) .or_default() .extend(post); } @@ -3127,60 +3868,67 @@ impl<'db> SequentMap<'db> { } } - fn pair_key( - db: &'db dyn Db, - ante1: ConstrainedTypeVar<'db>, - ante2: ConstrainedTypeVar<'db>, - ) -> (ConstrainedTypeVar<'db>, ConstrainedTypeVar<'db>) { - if ante1.ordering(db) < ante2.ordering(db) { + fn pair_key(ante1: ConstraintId, ante2: ConstraintId) -> (ConstraintId, ConstraintId) { + if ante1.ordering() < ante2.ordering() { (ante1, ante2) } else { (ante2, ante1) } } - fn add_single_tautology(&mut self, db: &'db dyn Db, ante: ConstrainedTypeVar<'db>) { + fn add_single_tautology<'db>( + &mut self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + ante: ConstraintId, + ) { if self.single_tautologies.insert(ante) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - sequent = %format_args!("¬{} → false", ante.display(db)), + sequent = %format_args!("¬{} → false", ante.display(db, builder)), "add sequent", ); } } - fn add_pair_impossibility( + fn add_pair_impossibility<'db>( &mut self, db: &'db dyn Db, - ante1: ConstrainedTypeVar<'db>, - ante2: ConstrainedTypeVar<'db>, + builder: &ConstraintSetBuilder<'db>, + ante1: ConstraintId, + ante2: ConstraintId, ) { if self .pair_impossibilities - .insert(Self::pair_key(db, ante1, ante2)) + .insert(Self::pair_key(ante1, ante2)) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - sequent = %format_args!("{} ∧ {} → false", ante1.display(db), ante2.display(db)), + sequent = %format_args!( + "{} ∧ {} → false", + ante1.display(db, builder), + ante2.display(db, builder), + ), "add sequent", ); } } - fn add_pair_implication( + fn add_pair_implication<'db>( &mut self, db: &'db dyn Db, - ante1: ConstrainedTypeVar<'db>, - ante2: ConstrainedTypeVar<'db>, - post: ConstrainedTypeVar<'db>, + builder: &ConstraintSetBuilder<'db>, + ante1: ConstraintId, + ante2: ConstraintId, + post: ConstraintId, ) { // If either antecedent implies the consequent on its own, this new sequent is redundant. - if ante1.implies(db, post) || ante2.implies(db, post) { + if ante1.implies(db, builder, post) || ante2.implies(db, builder, post) { return; } if self .pair_implications - .entry(Self::pair_key(db, ante1, ante2)) + .entry(Self::pair_key(ante1, ante2)) .or_default() .insert(post) { @@ -3188,20 +3936,21 @@ impl<'db> SequentMap<'db> { target: "ty_python_semantic::types::constraints::SequentMap", sequent = %format_args!( "{} ∧ {} → {}", - ante1.display(db), - ante2.display(db), - post.display(db), + ante1.display(db, builder), + ante2.display(db, builder), + post.display(db, builder), ), "add sequent", ); } } - fn add_single_implication( + fn add_single_implication<'db>( &mut self, db: &'db dyn Db, - ante: ConstrainedTypeVar<'db>, - post: ConstrainedTypeVar<'db>, + builder: &ConstraintSetBuilder<'db>, + ante: ConstraintId, + post: ConstraintId, ) { if ante == post { return; @@ -3216,21 +3965,27 @@ impl<'db> SequentMap<'db> { target: "ty_python_semantic::types::constraints::SequentMap", sequent = %format_args!( "{} → {}", - ante.display(db), - post.display(db), + ante.display(db, builder), + post.display(db, builder), ), "add sequent", ); } } - fn add_sequents_for_single(&mut self, db: &'db dyn Db, constraint: ConstrainedTypeVar<'db>) { + fn add_sequents_for_single<'db>( + &mut self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + constraint: ConstraintId, + ) { // If this constraint binds its typevar to `Never ≤ T ≤ object`, then the typevar can take // on any type, and the constraint is always satisfied. - let lower = constraint.lower(db); - let upper = constraint.upper(db); + let constraint_data = builder.constraint_data(constraint); + let lower = constraint_data.lower; + let upper = constraint_data.upper; if lower.is_never() && upper.is_object() { - self.add_single_tautology(db, constraint); + self.add_single_tautology(db, builder, constraint); return; } @@ -3247,34 +4002,53 @@ impl<'db> SequentMap<'db> { let post_constraint = match (lower, upper) { // Case 1 (Type::TypeVar(lower_typevar), Type::TypeVar(upper_typevar)) => { - if !lower_typevar.is_same_typevar_as(db, upper_typevar) { - ConstrainedTypeVar::new(db, lower_typevar, Type::Never, upper) - } else { + if lower_typevar.is_same_typevar_as(db, upper_typevar) { return; } + + // We always want to propagate `lower ≤ upper`, but we must do so using a + // canonical top-level typevar ordering. + // + // Example: if we learn `(A ≤ [T] ≤ B)`, this single-constraint propagation step + // should infer `A ≤ B`. Depending on ordering, we might need to encode that as + // either `(Never ≤ [A] ≤ B)` or `(A ≤ [B] ≤ object)`. Both render as `A ≤ B`, + // but they constrain different typevars and must be created in the orientation + // allowed by `can_be_bound_for`. + if upper_typevar.can_be_bound_for(db, builder, lower_typevar) { + ConstraintId::new(db, builder, lower_typevar, Type::Never, upper) + } else { + ConstraintId::new( + db, + builder, + upper_typevar, + Type::TypeVar(lower_typevar), + Type::object(), + ) + } } // Case 2 (Type::TypeVar(lower_typevar), _) => { - ConstrainedTypeVar::new(db, lower_typevar, Type::Never, upper) + ConstraintId::new(db, builder, lower_typevar, Type::Never, upper) } // Case 3 (_, Type::TypeVar(upper_typevar)) => { - ConstrainedTypeVar::new(db, upper_typevar, lower, Type::object()) + ConstraintId::new(db, builder, upper_typevar, lower, Type::object()) } _ => return, }; - self.add_single_implication(db, constraint, post_constraint); + self.add_single_implication(db, builder, constraint, post_constraint); } - fn add_sequents_for_pair( + fn add_sequents_for_pair<'db>( &mut self, db: &'db dyn Db, - left_constraint: ConstrainedTypeVar<'db>, - right_constraint: ConstrainedTypeVar<'db>, + builder: &ConstraintSetBuilder<'db>, + left_constraint: ConstraintId, + right_constraint: ConstraintId, ) { // If either of the constraints has another typevar as a lower/upper bound, the only // sequents we can add are for the transitive closure. For instance, if we have @@ -3297,79 +4071,88 @@ impl<'db> SequentMap<'db> { // // If all of the lower and upper bounds are concrete (i.e., not typevars), then there // several _other_ sequents that we can add, as handled by `add_concrete_sequents`. - let left_typevar = left_constraint.typevar(db); - let right_typevar = right_constraint.typevar(db); + let left_constraint_data = builder.constraint_data(left_constraint); + let left_typevar = left_constraint_data.typevar; + let right_constraint_data = builder.constraint_data(right_constraint); + let right_typevar = right_constraint_data.typevar; if !left_typevar.is_same_typevar_as(db, right_typevar) { - self.add_mutual_sequents_for_different_typevars(db, left_constraint, right_constraint); - } else if left_constraint.lower(db).is_type_var() - || left_constraint.upper(db).is_type_var() - || right_constraint.lower(db).is_type_var() - || right_constraint.upper(db).is_type_var() + self.add_mutual_sequents_for_different_typevars( + db, + builder, + left_constraint, + right_constraint, + ); + } else if left_constraint_data.lower.is_type_var() + || left_constraint_data.upper.is_type_var() + || right_constraint_data.lower.is_type_var() + || right_constraint_data.upper.is_type_var() { - self.add_mutual_sequents_for_same_typevars(db, left_constraint, right_constraint); + self.add_mutual_sequents_for_same_typevars( + db, + builder, + left_constraint, + right_constraint, + ); } else { - self.add_concrete_sequents(db, left_constraint, right_constraint); + self.add_concrete_sequents(db, builder, left_constraint, right_constraint); } } - fn add_mutual_sequents_for_different_typevars( + fn add_mutual_sequents_for_different_typevars<'db>( &mut self, db: &'db dyn Db, - left_constraint: ConstrainedTypeVar<'db>, - right_constraint: ConstrainedTypeVar<'db>, + builder: &ConstraintSetBuilder<'db>, + left_constraint: ConstraintId, + right_constraint: ConstraintId, ) { // We've structured our constraints so that a typevar's upper/lower bound can only // be another typevar if the bound is "later" in our arbitrary ordering. That means // we only have to check this pair of constraints in one direction — though we do // have to figure out which of the two typevars is constrained, and which one is // the upper/lower bound. - let left_typevar = left_constraint.typevar(db); - let right_typevar = right_constraint.typevar(db); - let (bound_typevar, bound_constraint, constrained_typevar, constrained_constraint) = - if left_typevar.can_be_bound_for(db, right_typevar) { - ( - left_typevar, - left_constraint, - right_typevar, - right_constraint, - ) + let left_constraint_data = builder.constraint_data(left_constraint); + let left_typevar = left_constraint_data.typevar; + let right_constraint_data = builder.constraint_data(right_constraint); + let right_typevar = right_constraint_data.typevar; + let (bound_constraint, constrained_constraint) = + if left_typevar.can_be_bound_for(db, builder, right_typevar) { + (left_constraint, right_constraint) } else { - ( - right_typevar, - right_constraint, - left_typevar, - left_constraint, - ) + (right_constraint, left_constraint) }; // We then look for cases where the "constrained" typevar's upper and/or lower bound // matches the "bound" typevar. If so, we're going to add an implication sequent that // replaces the upper/lower bound that matched with the bound constraint's corresponding // bound. + let bound_constraint_data = builder.constraint_data(bound_constraint); + let bound_typevar = bound_constraint_data.typevar; + let constrained_constraint_data = builder.constraint_data(constrained_constraint); + let constrained_typevar = constrained_constraint_data.typevar; let (new_lower, new_upper) = match ( - constrained_constraint.lower(db), - constrained_constraint.upper(db), + constrained_constraint_data.lower, + constrained_constraint_data.upper, ) { // (B ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ BU) (Type::TypeVar(constrained_lower), Type::TypeVar(constrained_upper)) if constrained_lower.is_same_typevar_as(db, bound_typevar) && constrained_upper.is_same_typevar_as(db, bound_typevar) => { - (bound_constraint.lower(db), bound_constraint.upper(db)) + (bound_constraint_data.lower, bound_constraint_data.upper) } // (CL ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (CL ≤ C ≤ BU) (constrained_lower, Type::TypeVar(constrained_upper)) if constrained_upper.is_same_typevar_as(db, bound_typevar) => { - (constrained_lower, bound_constraint.upper(db)) + (constrained_lower, bound_constraint_data.upper) } // (B ≤ C ≤ CU) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ CU) (Type::TypeVar(constrained_lower), constrained_upper) if constrained_lower.is_same_typevar_as(db, bound_typevar) => { - (bound_constraint.lower(db), constrained_upper) + (bound_constraint_data.lower, constrained_upper) } // (CL ≤ C ≤ pivot) ∧ (pivot ≤ B ≤ BU) → (CL ≤ C ≤ B) @@ -3380,7 +4163,7 @@ impl<'db> SequentMap<'db> { .top_materialization(db) .is_constraint_set_assignable_to( db, - bound_constraint.lower(db).bottom_materialization(db), + bound_constraint_data.lower.bottom_materialization(db), ) => { (constrained_lower, Type::TypeVar(bound_typevar)) @@ -3390,8 +4173,8 @@ impl<'db> SequentMap<'db> { (constrained_lower, constrained_upper) if !constrained_lower.is_never() && !constrained_lower.is_object() - && bound_constraint - .upper(db) + && bound_constraint_data + .upper .top_materialization(db) .is_constraint_set_assignable_to( db, @@ -3404,111 +4187,232 @@ impl<'db> SequentMap<'db> { _ => return, }; - let post_constraint = - ConstrainedTypeVar::new(db, constrained_typevar, new_lower, new_upper); - self.add_pair_implication(db, left_constraint, right_constraint, post_constraint); + let mut post_constraints: SmallVec<[ConstraintId; 3]> = SmallVec::new(); + let mut constrained_lower = new_lower; + let mut constrained_upper = new_upper; + + // The transitive rule above gives us an intended post-condition + // `new_lower ≤ [constrained] ≤ new_upper`. + // + // If a top-level bound typevar is "earlier" than `constrained`, we cannot represent that + // directly as a bound on `constrained` without violating our canonical ordering. + // Instead, split it into equivalent canonical constraints by "moving" that bound onto the + // other typevar: + // + // invalid lower `L ≤ [C]` -> `(Never ≤ [L] ≤ C)` and drop `L` from C's lower bound + // invalid upper `[C] ≤ U` -> `(C ≤ [U] ≤ object)` and drop `U` from C's upper bound + // + // Example: if we derive `[A] ≤ T ≤ [B]` but `A`/`B` are not valid top-level bounds for + // `T` in this ordering, we emit two pair implications: + // `(Never ≤ [A] ≤ T)` and `(T ≤ [B] ≤ object)`. + // This preserves the relationship while keeping all derived constraints canonical. + if let Type::TypeVar(lower_bound_typevar) = new_lower + && !lower_bound_typevar.can_be_bound_for(db, builder, constrained_typevar) + { + post_constraints.push(ConstraintId::new( + db, + builder, + lower_bound_typevar, + Type::Never, + Type::TypeVar(constrained_typevar), + )); + constrained_lower = Type::Never; + } + + if let Type::TypeVar(upper_bound_typevar) = new_upper + && !upper_bound_typevar.can_be_bound_for(db, builder, constrained_typevar) + { + post_constraints.push(ConstraintId::new( + db, + builder, + upper_bound_typevar, + Type::TypeVar(constrained_typevar), + Type::object(), + )); + constrained_upper = Type::object(); + } + + if !(constrained_lower.is_never() && constrained_upper.is_object()) { + post_constraints.push(ConstraintId::new( + db, + builder, + constrained_typevar, + constrained_lower, + constrained_upper, + )); + } + + for post_constraint in post_constraints { + self.add_pair_implication( + db, + builder, + left_constraint, + right_constraint, + post_constraint, + ); + } } - fn add_mutual_sequents_for_same_typevars( + fn add_mutual_sequents_for_same_typevars<'db>( &mut self, db: &'db dyn Db, - left_constraint: ConstrainedTypeVar<'db>, - right_constraint: ConstrainedTypeVar<'db>, + builder: &ConstraintSetBuilder<'db>, + left_constraint: ConstraintId, + right_constraint: ConstraintId, ) { let mut try_one_direction = - |left_constraint: ConstrainedTypeVar<'db>, - right_constraint: ConstrainedTypeVar<'db>| { - let left_lower = left_constraint.lower(db); - let left_upper = left_constraint.upper(db); - let right_lower = right_constraint.lower(db); - let right_upper = right_constraint.upper(db); - let new_constraint = |bound_typevar: BoundTypeVarInstance<'db>, - right_lower: Type<'db>, - right_upper: Type<'db>| { - let right_lower = if let Type::TypeVar(other_bound_typevar) = right_lower - && bound_typevar.is_same_typevar_as(db, other_bound_typevar) - { - Type::Never - } else { - right_lower - }; - let right_upper = if let Type::TypeVar(other_bound_typevar) = right_upper - && bound_typevar.is_same_typevar_as(db, other_bound_typevar) - { - Type::object() - } else { - right_upper + |left_constraint: ConstraintId, right_constraint: ConstraintId| { + let left_constraint_data = builder.constraint_data(left_constraint); + let left_lower = left_constraint_data.lower; + let left_upper = left_constraint_data.upper; + let right_constraint_data = builder.constraint_data(right_constraint); + let right_lower = right_constraint_data.lower; + let right_upper = right_constraint_data.upper; + let new_constraints = + |bound_typevar: BoundTypeVarInstance<'db>, + right_lower: Type<'db>, + right_upper: Type<'db>| { + let right_lower = if let Type::TypeVar(other_bound_typevar) = right_lower + && bound_typevar.is_same_typevar_as(db, other_bound_typevar) + { + Type::Never + } else { + right_lower + }; + let right_upper = if let Type::TypeVar(other_bound_typevar) = right_upper + && bound_typevar.is_same_typevar_as(db, other_bound_typevar) + { + Type::object() + } else { + right_upper + }; + + // Same idea as `add_mutual_sequents_for_different_typevars`: if a derived + // post-condition for `[bound]` has top-level typevar bounds in the wrong + // orientation, split it into equivalent canonical constraints instead of + // dropping it. + let mut post_constraints: SmallVec<[ConstraintId; 3]> = SmallVec::new(); + let mut constrained_lower = right_lower; + let mut constrained_upper = right_upper; + + if let Type::TypeVar(lower_bound_typevar) = right_lower + && !lower_bound_typevar.can_be_bound_for(db, builder, bound_typevar) + { + post_constraints.push(ConstraintId::new( + db, + builder, + lower_bound_typevar, + Type::Never, + Type::TypeVar(bound_typevar), + )); + constrained_lower = Type::Never; + } + + if let Type::TypeVar(upper_bound_typevar) = right_upper + && !upper_bound_typevar.can_be_bound_for(db, builder, bound_typevar) + { + post_constraints.push(ConstraintId::new( + db, + builder, + upper_bound_typevar, + Type::TypeVar(bound_typevar), + Type::object(), + )); + constrained_upper = Type::object(); + } + + if !(constrained_lower.is_never() && constrained_upper.is_object()) { + post_constraints.push(ConstraintId::new( + db, + builder, + bound_typevar, + constrained_lower, + constrained_upper, + )); + } + + post_constraints }; - ConstrainedTypeVar::new(db, bound_typevar, right_lower, right_upper) - }; - let post_constraint = match (left_lower, left_upper) { + let post_constraints = match (left_lower, left_upper) { (Type::TypeVar(bound_typevar), Type::TypeVar(other_bound_typevar)) if bound_typevar.is_same_typevar_as(db, other_bound_typevar) => { - new_constraint(bound_typevar, right_lower, right_upper) + new_constraints(bound_typevar, right_lower, right_upper) } (Type::TypeVar(bound_typevar), _) => { - new_constraint(bound_typevar, Type::Never, right_upper) + new_constraints(bound_typevar, Type::Never, right_upper) } (_, Type::TypeVar(bound_typevar)) => { - new_constraint(bound_typevar, right_lower, Type::object()) + new_constraints(bound_typevar, right_lower, Type::object()) } _ => return, }; - self.add_pair_implication(db, left_constraint, right_constraint, post_constraint); + for post_constraint in post_constraints { + self.add_pair_implication( + db, + builder, + left_constraint, + right_constraint, + post_constraint, + ); + } }; try_one_direction(left_constraint, right_constraint); try_one_direction(right_constraint, left_constraint); } - fn add_concrete_sequents( + fn add_concrete_sequents<'db>( &mut self, db: &'db dyn Db, - left_constraint: ConstrainedTypeVar<'db>, - right_constraint: ConstrainedTypeVar<'db>, + builder: &ConstraintSetBuilder<'db>, + left_constraint: ConstraintId, + right_constraint: ConstraintId, ) { // These might seem redundant with the intersection check below, since `a → b` means that // `a ∧ b = a`. But we are not normalizing constraint bounds, and these clauses help us // identify constraints that are identical besides e.g. ordering of union/intersection // elements. (For instance, when processing `T ≤ τ₁ & τ₂` and `T ≤ τ₂ & τ₁`, these clauses // would add sequents for `(T ≤ τ₁ & τ₂) → (T ≤ τ₂ & τ₁)` and vice versa.) - if left_constraint.implies(db, right_constraint) { + if left_constraint.implies(db, builder, right_constraint) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db), - right = %right_constraint.display(db), + left = %left_constraint.display(db, builder), + right = %right_constraint.display(db, builder), "left implies right", ); - self.add_single_implication(db, left_constraint, right_constraint); + self.add_single_implication(db, builder, left_constraint, right_constraint); } - if right_constraint.implies(db, left_constraint) { + if right_constraint.implies(db, builder, left_constraint) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db), - right = %right_constraint.display(db), + left = %left_constraint.display(db, builder), + right = %right_constraint.display(db, builder), "right implies left", ); - self.add_single_implication(db, right_constraint, left_constraint); + self.add_single_implication(db, builder, right_constraint, left_constraint); } - match left_constraint.intersect(db, right_constraint) { - IntersectionResult::Simplified(intersection_constraint) => { + match left_constraint.intersect(db, builder, right_constraint) { + IntersectionResult::Simplified(intersection_constraint_data) => { + let intersection_constraint = + builder.intern_constraint(db, intersection_constraint_data); tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db), - right = %right_constraint.display(db), - intersection = %intersection_constraint.display(db), + left = %left_constraint.display(db, builder), + right = %right_constraint.display(db, builder), + intersection = %intersection_constraint.display(db, builder), "left and right overlap", ); self.add_pair_implication( db, + builder, left_constraint, right_constraint, intersection_constraint, ); - self.add_single_implication(db, intersection_constraint, left_constraint); - self.add_single_implication(db, intersection_constraint, right_constraint); + self.add_single_implication(db, builder, intersection_constraint, left_constraint); + self.add_single_implication(db, builder, intersection_constraint, right_constraint); } // The sequent map only needs to include constraints that might appear in a BDD. If the @@ -3519,21 +4423,27 @@ impl<'db> SequentMap<'db> { IntersectionResult::Disjoint => { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db), - right = %right_constraint.display(db), + left = %left_constraint.display(db, builder), + right = %right_constraint.display(db, builder), "left and right are disjoint", ); - self.add_pair_impossibility(db, left_constraint, right_constraint); + self.add_pair_impossibility(db, builder, left_constraint, right_constraint); } } } #[expect(dead_code)] // Keep this around for debugging purposes - fn display<'a>(&'a self, db: &'db dyn Db, prefix: &'a dyn Display) -> impl Display + 'a { + fn display<'db, 'a>( + &'a self, + db: &'db dyn Db, + builder: &'a ConstraintSetBuilder<'db>, + prefix: &'a dyn Display, + ) -> impl Display + 'a { struct DisplaySequentMap<'a, 'db> { - map: &'a SequentMap<'db>, + map: &'a SequentMap, prefix: &'a dyn Display, db: &'db dyn Db, + builder: &'a ConstraintSetBuilder<'db>, } impl Display for DisplaySequentMap<'_, '_> { @@ -3553,8 +4463,8 @@ impl<'db> SequentMap<'db> { write!( f, "{} ∧ {} → false", - ante1.display(self.db), - ante2.display(self.db), + ante1.display(self.db, self.builder), + ante2.display(self.db, self.builder), )?; } @@ -3564,9 +4474,9 @@ impl<'db> SequentMap<'db> { write!( f, "{} ∧ {} → {}", - ante1.display(self.db), - ante2.display(self.db), - post.display(self.db), + ante1.display(self.db, self.builder), + ante2.display(self.db, self.builder), + post.display(self.db, self.builder), )?; } } @@ -3574,7 +4484,12 @@ impl<'db> SequentMap<'db> { for (ante, posts) in &self.map.single_implications { for post in posts { maybe_write_prefix(f)?; - write!(f, "{} → {}", ante.display(self.db), post.display(self.db))?; + write!( + f, + "{} → {}", + ante.display(self.db, self.builder), + post.display(self.db, self.builder) + )?; } } @@ -3589,6 +4504,7 @@ impl<'db> SequentMap<'db> { map: self, prefix, db, + builder, } } } @@ -3596,17 +4512,17 @@ impl<'db> SequentMap<'db> { /// The collection of constraints that we know to be true or false at a certain point when /// traversing a BDD. #[derive(Debug)] -pub(crate) struct PathAssignments<'db> { - map: SequentMap<'db>, - assignments: FxIndexMap, usize>, +pub(crate) struct PathAssignments { + map: SequentMap, + assignments: FxIndexMap, /// Constraints that we have discovered, mapped to whether we have processed them yet. (This /// ensures a stable order for all of the derived constraints that we create, while still /// letting us create them lazily.) - discovered: FxIndexMap, bool>, + discovered: FxIndexMap, } -impl<'db> PathAssignments<'db> { - fn new(constraints: impl IntoIterator>) -> Self { +impl PathAssignments { + fn new(constraints: impl IntoIterator) -> Self { let discovered = constraints .into_iter() .map(|constraint| (constraint, false)) @@ -3640,10 +4556,11 @@ impl<'db> PathAssignments<'db> { /// the BDD. You should make this call from inside of your callback, so that as you get further /// down into the BDD structure, we remember all of the information that we have learned from /// the path we're on. - fn walk_edge( + fn walk_edge<'db, R>( &mut self, db: &'db dyn Db, - assignment: ConstraintAssignment<'db>, + builder: &ConstraintSetBuilder<'db>, + assignment: ConstraintAssignment, source_order: usize, f: impl FnOnce(&mut Self, Range) -> R, ) -> Option { @@ -3657,12 +4574,14 @@ impl<'db> PathAssignments<'db> { target: "ty_python_semantic::types::constraints::PathAssignment", before = %format_args!( "[{}]", - self.assignments[..start].iter().map(|(assignment, _)| assignment.display(db)).format(", "), + self.assignments[..start].iter().map(|(assignment, _)| { + assignment.display(db, builder) + }).format(", "), ), - edge = %assignment.display(db), + edge = %assignment.display(db, builder), "walk edge", ); - let found_conflict = self.add_assignment(db, assignment, source_order); + let found_conflict = self.add_assignment(db, builder, assignment, source_order); let result = if found_conflict.is_err() { // If that results in the path now being impossible due to a contradiction, return // without invoking the callback. @@ -3677,7 +4596,9 @@ impl<'db> PathAssignments<'db> { target: "ty_python_semantic::types::constraints::PathAssignment", new = %format_args!( "[{}]", - self.assignments[start..].iter().map(|(assignment, _)| assignment.display(db)).format(", "), + self.assignments[start..].iter().map(|(assignment, _)| { + assignment.display(db, builder) + }).format(", "), ), "new assignments", ); @@ -3691,9 +4612,7 @@ impl<'db> PathAssignments<'db> { result } - pub(crate) fn positive_constraints( - &self, - ) -> impl Iterator, usize)> + '_ { + pub(crate) fn positive_constraints(&self) -> impl Iterator + '_ { self.assignments .iter() .filter_map(|(assignment, source_order)| match assignment { @@ -3702,7 +4621,7 @@ impl<'db> PathAssignments<'db> { }) } - fn assignment_holds(&self, assignment: ConstraintAssignment<'db>) -> bool { + fn assignment_holds(&self, assignment: ConstraintAssignment) -> bool { self.assignments.contains_key(&assignment) } @@ -3711,7 +4630,12 @@ impl<'db> PathAssignments<'db> { /// [`SequentMap::for_constraint`] and [`for_constraint_pair`][SequentMap::for_constraint_pair] /// to calculate _and cache_ the constraints, so that if we walk another constraint set /// containing this constraint, we reuse the work to calculate its sequents. - fn discover_constraint(&mut self, db: &'db dyn Db, constraint: ConstrainedTypeVar<'db>) { + fn discover_constraint<'db>( + &mut self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + constraint: ConstraintId, + ) { // If we've already processed this constraint, we can skip it. let existing = self.discovered.insert(constraint, true); let already_processed = existing.is_some_and(|existing| existing); @@ -3719,22 +4643,24 @@ impl<'db> PathAssignments<'db> { return; } - let single_map = SequentMap::for_constraint(db, constraint); - self.map.merge(db, single_map); + let single_map = SequentMap::for_constraint(db, builder, constraint); + self.map.merge(&single_map); + drop(single_map); for existing in self.discovered.keys().dropping_back(1) { - let pair_map = SequentMap::for_constraint_pair(db, *existing, constraint); - self.map.merge(db, pair_map); + let pair_map = SequentMap::for_constraint_pair(db, builder, *existing, constraint); + self.map.merge(&pair_map); } } /// Adds a new assignment, along with any derived information that we can infer from the new /// assignment combined with the assignments we've already seen. If any of this causes the path /// to become invalid, due to a contradiction, returns a [`PathAssignmentConflict`] error. - fn add_assignment( + fn add_assignment<'db>( &mut self, db: &'db dyn Db, - assignment: ConstraintAssignment<'db>, + builder: &ConstraintSetBuilder<'db>, + assignment: ConstraintAssignment, source_order: usize, ) -> Result<(), PathAssignmentConflict> { // First add this assignment. If it causes a conflict, return that as an error. If we've @@ -3742,10 +4668,12 @@ impl<'db> PathAssignments<'db> { if self.assignments.contains_key(&assignment.negated()) { tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - assignment = %assignment.display(db), + assignment = %assignment.display(db, builder), facts = %format_args!( "[{}]", - self.assignments.iter().map(|(assignment, _)| assignment.display(db)).format(", "), + self.assignments.iter().map(|(assignment, _)| { + assignment.display(db, builder) + }).format(", "), ), "found contradiction", ); @@ -3769,7 +4697,7 @@ impl<'db> PathAssignments<'db> { // don't anticipate the sequent maps to be very large. We might consider avoiding the // brute-force search. - self.discover_constraint(db, assignment.constraint()); + self.discover_constraint(db, builder, assignment.constraint()); for ante in &self.map.single_tautologies { if self.assignment_holds(ante.when_false()) { @@ -3777,10 +4705,12 @@ impl<'db> PathAssignments<'db> { // it's false. tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - ante = %ante.display(db), + ante = %ante.display(db, builder), facts = %format_args!( "[{}]", - self.assignments.iter().map(|(assignment, _)| assignment.display(db)).format(", "), + self.assignments.iter().map(|(assignment, _)| { + assignment.display(db, builder) + }).format(", "), ), "found contradiction", ); @@ -3795,11 +4725,13 @@ impl<'db> PathAssignments<'db> { // current path asserts that both are true. tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - ante1 = %ante1.display(db), - ante2 = %ante2.display(db), + ante1 = %ante1.display(db, builder), + ante2 = %ante2.display(db, builder), facts = %format_args!( "[{}]", - self.assignments.iter().map(|(assignment, _)| assignment.display(db)).format(", "), + self.assignments.iter().map(|(assignment, _)| { + assignment.display(db, builder) + }).format(", "), ), "found contradiction", ); @@ -3827,7 +4759,7 @@ impl<'db> PathAssignments<'db> { } for new_constraint in new_constraints { - self.add_assignment(db, new_constraint.when_true(), source_order)?; + self.add_assignment(db, builder, new_constraint.when_true(), source_order)?; } Ok(()) @@ -3839,12 +4771,12 @@ struct PathAssignmentConflict; /// A single clause in the DNF representation of a BDD #[derive(Clone, Debug, Default, Eq, PartialEq)] -struct SatisfiedClause<'db> { - constraints: Vec>, +struct SatisfiedClause { + constraints: Vec, } -impl<'db> SatisfiedClause<'db> { - fn push(&mut self, constraint: ConstraintAssignment<'db>) { +impl SatisfiedClause { + fn push(&mut self, constraint: ConstraintAssignment) { self.constraints.push(constraint); } @@ -3868,7 +4800,7 @@ impl<'db> SatisfiedClause<'db> { /// Removes another clause from this clause, if it appears as a prefix of this clause. Returns /// whether the prefix was removed. - fn remove_prefix(&mut self, prefix: &SatisfiedClause<'db>) -> bool { + fn remove_prefix(&mut self, prefix: &SatisfiedClause) -> bool { if self.constraints.starts_with(&prefix.constraints) { self.constraints.drain(0..prefix.constraints.len()); return true; @@ -3881,7 +4813,7 @@ impl<'db> SatisfiedClause<'db> { /// want to remove the larger one and keep the smaller one.) /// /// Returns a boolean that indicates whether any simplifications were made. - fn simplify(&mut self, db: &'db dyn Db) -> bool { + fn simplify<'db>(&mut self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> bool { let mut changes_made = false; let mut i = 0; // Loop through each constraint, comparing it with any constraints that appear later in the @@ -3889,7 +4821,7 @@ impl<'db> SatisfiedClause<'db> { 'outer: while i < self.constraints.len() { let mut j = i + 1; while j < self.constraints.len() { - if self.constraints[j].implies(db, self.constraints[i]) { + if self.constraints[j].implies(db, builder, self.constraints[i]) { // If constraint `i` is removed, then we don't need to compare it with any // later constraints in the list. Note that we continue the outer loop, instead // of breaking from the inner loop, so that we don't bump index `i` below. @@ -3898,7 +4830,7 @@ impl<'db> SatisfiedClause<'db> { self.constraints.swap_remove(i); changes_made = true; continue 'outer; - } else if self.constraints[i].implies(db, self.constraints[j]) { + } else if self.constraints[i].implies(db, builder, self.constraints[j]) { // If constraint `j` is removed, then we can continue the inner loop. We will // swap a new element into place at index `j`, and will continue comparing the // constraint at index `i` with later constraints. @@ -3913,7 +4845,7 @@ impl<'db> SatisfiedClause<'db> { changes_made } - fn display(&self, db: &'db dyn Db) -> String { + fn display<'db>(&self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> String { if self.constraints.is_empty() { return String::from("always"); } @@ -3925,9 +4857,11 @@ impl<'db> SatisfiedClause<'db> { .constraints .iter() .map(|constraint| match constraint { - ConstraintAssignment::Positive(constraint) => constraint.display(db).to_string(), + ConstraintAssignment::Positive(constraint) => { + constraint.display(db, builder).to_string() + } ConstraintAssignment::Negative(constraint) => { - constraint.display_negated(db).to_string() + constraint.display_negated(db, builder).to_string() } }) .collect(); @@ -3953,23 +4887,23 @@ impl<'db> SatisfiedClause<'db> { /// A list of the clauses that satisfy a BDD. This is a DNF representation of the boolean function /// that the BDD represents. #[derive(Clone, Debug, Default, Eq, PartialEq)] -struct SatisfiedClauses<'db> { - clauses: Vec>, +struct SatisfiedClauses { + clauses: Vec, } -impl<'db> SatisfiedClauses<'db> { - fn push(&mut self, clause: SatisfiedClause<'db>) { +impl SatisfiedClauses { + fn push(&mut self, clause: SatisfiedClause) { self.clauses.push(clause); } /// Simplifies the DNF representation, removing redundancies that do not change the underlying /// function. (This is used when displaying a BDD, to make sure that the representation that we /// show is as simple as possible while still producing the same results.) - fn simplify(&mut self, db: &'db dyn Db) { + fn simplify<'db>(&mut self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) { // First simplify each clause individually, by removing constraints that are implied by // other constraints in the clause. for clause in &mut self.clauses { - clause.simplify(db); + clause.simplify(db, builder); } while self.simplify_one_round() { @@ -4041,7 +4975,7 @@ impl<'db> SatisfiedClauses<'db> { false } - fn display(&self, db: &'db dyn Db) -> String { + fn display<'db>(&self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> String { // This is a bit heavy-handed, but we need to output the clauses in a consistent order // even though Salsa IDs are assigned non-deterministically. This Display output is only // used in test cases, so we don't need to over-optimize it. @@ -4052,7 +4986,7 @@ impl<'db> SatisfiedClauses<'db> { let mut clauses: Vec<_> = self .clauses .iter() - .map(|clause| clause.display(db)) + .map(|clause| clause.display(db, builder)) .collect(); clauses.sort(); clauses.join(" ∨ ") @@ -4063,10 +4997,10 @@ impl<'db> BoundTypeVarInstance<'db> { /// Returns the valid specializations of a typevar. This is used when checking a constraint set /// when this typevar is in inferable position, where we only need _some_ specialization to /// satisfy the constraint set. - fn valid_specializations(self, db: &'db dyn Db) -> Node<'db> { + fn valid_specializations(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { if self.paramspec_attr(db).is_some() { // P.args and P.kwargs are variadic, and do not have an upper bound or constraints. - return Node::AlwaysTrue; + return ALWAYS_TRUE; } // For gradual upper bounds and constraints, we are free to choose any materialization that @@ -4080,19 +5014,19 @@ impl<'db> BoundTypeVarInstance<'db> { // that _some_ valid specialization satisfies the constraint set, it's correct for us to // return the range of valid materializations that we can choose from. match self.typevar(db).bound_or_constraints(db) { - None => Node::AlwaysTrue, + None => ALWAYS_TRUE, Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let bound = bound.top_materialization(db); - ConstrainedTypeVar::new_node(db, self, Type::Never, bound) + Constraint::new_node(db, builder, self, Type::Never, bound) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut specializations = Node::AlwaysFalse; + let mut specializations = ALWAYS_FALSE; for constraint in constraints.elements(db) { let constraint_lower = constraint.bottom_materialization(db); let constraint_upper = constraint.top_materialization(db); specializations = specializations.or_with_offset( - db, - ConstrainedTypeVar::new_node(db, self, constraint_lower, constraint_upper), + builder, + Constraint::new_node(db, builder, self, constraint_lower, constraint_upper), ); } specializations @@ -4113,32 +5047,36 @@ impl<'db> BoundTypeVarInstance<'db> { /// specifies the required specializations, and the iterator will be empty. For a constrained /// typevar, the primary result will include the fully static constraints, and the iterator /// will include an entry for each non-fully-static constraint. - fn required_specializations(self, db: &'db dyn Db) -> (Node<'db>, Vec>) { + fn required_specializations( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + ) -> (NodeId, Vec) { // For upper bounds and constraints, we are free to choose any materialization that makes // the check succeed. In non-inferable positions, it is most helpful to choose a // materialization that is as restrictive as possible, since that minimizes the number of // valid specializations that must satisfy the check. We therefore take the bottom // materialization of the bound or constraints. match self.typevar(db).bound_or_constraints(db) { - None => (Node::AlwaysTrue, Vec::new()), + None => (ALWAYS_TRUE, Vec::new()), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let bound = bound.bottom_materialization(db); ( - ConstrainedTypeVar::new_node(db, self, Type::Never, bound), + Constraint::new_node(db, builder, self, Type::Never, bound), Vec::new(), ) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut non_gradual_constraints = Node::AlwaysFalse; + let mut non_gradual_constraints = ALWAYS_FALSE; let mut gradual_constraints = Vec::new(); for constraint in constraints.elements(db) { let constraint_lower = constraint.bottom_materialization(db); let constraint_upper = constraint.top_materialization(db); let constraint = - ConstrainedTypeVar::new_node(db, self, constraint_lower, constraint_upper); + Constraint::new_node(db, builder, self, constraint_lower, constraint_upper); if constraint_lower == constraint_upper { non_gradual_constraints = - non_gradual_constraints.or_with_offset(db, constraint); + non_gradual_constraints.or_with_offset(builder, constraint); } else { gradual_constraints.push(constraint); } @@ -4193,9 +5131,9 @@ mod tests { let u_bool = ConstraintSet::constrain_typevar(&db, &constraints, u, bool_type, bool_type); // Construct this in a different order than above to make the source_orders more // interesting. - let constraints = (u_str.or(&db, &constraints, || u_bool)) + let set = (u_str.or(&db, &constraints, || u_bool)) .and(&db, &constraints, || t_str.or(&db, &constraints, || t_bool)); - let actual = constraints.node.display_graph(&db, &"").to_string(); + let actual = set.node.display_graph(&db, &constraints, &"").to_string(); assert_eq!(actual, expected); } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index fd813063e15bc..119e4b73e4ddf 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1838,15 +1838,16 @@ impl<'db> SpecializationBuilder<'db> { fn add_type_mappings_from_constraint_set<'c>( &mut self, formal: Type<'db>, - constraints: ConstraintSet<'db, 'c>, + set: ConstraintSet<'db, 'c>, + constraints: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, ) -> Result<(), ()> { - let solutions = match constraints.solutions(self.db) { + let solutions = match set.solutions(self.db, constraints) { Solutions::Unsatisfiable => return Err(()), Solutions::Unconstrained => return Ok(()), Solutions::Constrained(solutions) => solutions, }; - for solution in solutions { + for solution in solutions.iter() { for binding in solution { let variance = formal.variance_of(self.db, binding.bound_typevar); self.add_type_mapping(binding.bound_typevar, binding.solution, variance, &mut f); @@ -1876,7 +1877,7 @@ impl<'db> SpecializationBuilder<'db> { &constraints, self.inferable, ); - self.add_type_mappings_from_constraint_set(formal, when, &mut *f)?; + self.add_type_mappings_from_constraint_set(formal, when, &constraints, &mut *f)?; } else { // An overloaded actual callable is compatible with the formal signature if at // least one of its overloads is. We collect type mappings from all satisfiable @@ -1890,7 +1891,7 @@ impl<'db> SpecializationBuilder<'db> { self.inferable, ); if self - .add_type_mappings_from_constraint_set(formal, when, &mut *f) + .add_type_mappings_from_constraint_set(formal, when, &constraints, &mut *f) .is_ok() { any_satisfiable = true; @@ -2328,7 +2329,12 @@ impl<'db> SpecializationBuilder<'db> { // unsatisfied comparisons simply produced no type mappings), and avoids // false positives for callable-wrapper patterns while this path is still // a hybrid of old and new solver logic. - let _ = self.add_type_mappings_from_constraint_set(formal, when, &mut f); + let _ = self.add_type_mappings_from_constraint_set( + formal, + when, + constraints, + &mut f, + ); return Ok(()); } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 0864c40f87d92..1145d434259df 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -14328,7 +14328,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (ast::UnaryOp::Invert, Type::KnownInstance(KnownInstanceType::ConstraintSet(set))) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let set = constraints.load(set.constraints(self.db())); + let set = constraints.load(self.db(), set.constraints(self.db())); set.negate(self.db(), constraints) }); Type::KnownInstance(KnownInstanceType::ConstraintSet( diff --git a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs index e43d880d81b3f..a297449d05d6c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs @@ -641,8 +641,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let left = constraints.load(left.constraints(db)); - let right = constraints.load(right.constraints(db)); + let left = constraints.load(db, left.constraints(db)); + let right = constraints.load(db, right.constraints(db)); left.and(db, constraints, || right) }); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( @@ -657,8 +657,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let left = constraints.load(left.constraints(db)); - let right = constraints.load(right.constraints(db)); + let left = constraints.load(db, left.constraints(db)); + let right = constraints.load(db, right.constraints(db)); left.or(db, constraints, || right) }); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index a1f3b109efdcd..45ecc78f989ef 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -531,8 +531,8 @@ pub(super) fn infer_binary_type_comparison<'db>( Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), ) => { let constraints = ConstraintSetBuilder::new(); - let left = constraints.load(left.constraints(db)); - let right = constraints.load(right.constraints(db)); + let left = constraints.load(db, left.constraints(db)); + let right = constraints.load(db, right.constraints(db)); let result = left.iff(db, &constraints, right); let equivalent = result.is_always_satisfied(db); match op { From 0c88d7f286cde119a61ec12c1374e6c9af5c8380 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Tue, 3 Mar 2026 00:02:23 -0800 Subject: [PATCH 175/261] [ty] filter out pre-loop bindings from loop headers (#23536) --- crates/ty_python_semantic/src/place.rs | 23 ++++++------- .../src/semantic_index/builder.rs | 33 ++++++++++++------- .../src/semantic_index/use_def.rs | 4 +++ .../src/types/infer/builder.rs | 8 ++--- 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 1d3c7ec66d96e..8837cf95d3361 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1208,13 +1208,15 @@ fn loop_header_reachability_impl<'db>( match use_def.definition(live_binding.binding) { DefinitionState::Defined(def) => { + debug_assert_ne!( + def, definition, + "loop headers only include bindings from within the loop" + ); has_defined_bindings = true; - if def != definition { - reachable_bindings.insert(ReachableLoopBinding { - definition: def, - narrowing_constraint: live_binding.narrowing_constraint, - }); - } + reachable_bindings.insert(ReachableLoopBinding { + definition: def, + narrowing_constraint: live_binding.narrowing_constraint, + }); } // `del` in the loop body is always visible to code after the loop via the // normal control flow merge. Updating `deleted_reachability` here is @@ -1222,10 +1224,9 @@ fn loop_header_reachability_impl<'db>( DefinitionState::Deleted => { deleted_reachability = deleted_reachability.or(reachability); } - // If UNBOUND is visible at loop-back, then it was visible before the loop. - // Loop header definitions don't shadow preexisting bindings, so we don't - // need to do anything with this. - DefinitionState::Undefined => {} + DefinitionState::Undefined => { + unreachable!("loop headers only include bindings from within the loop") + } } } @@ -1242,7 +1243,7 @@ pub(crate) struct LoopHeaderReachability<'db> { /// Whether any reachable loop-back binding is a defined binding. pub(crate) has_defined_bindings: bool, pub(crate) deleted_reachability: Truthiness, - /// Reachable, defined loop-back bindings (excluding the loop header definition itself). + /// Reachable loop-back bindings that are not `del`s. pub(crate) reachable_bindings: FxIndexSet>, } diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 71645d77565e8..ad6a5fe02f8d1 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -48,8 +48,8 @@ use crate::semantic_index::scope::{ use crate::semantic_index::scope::{Scope, ScopeId, ScopeKind, ScopeLaziness}; use crate::semantic_index::symbol::{ScopedSymbolId, Symbol}; use crate::semantic_index::use_def::{ - EnclosingSnapshotKey, FlowSnapshot, PreviousDefinitions, ScopedEnclosingSnapshotId, - UseDefMapBuilder, + EnclosingSnapshotKey, FlowSnapshot, PreviousDefinitions, ScopedDefinitionId, + ScopedEnclosingSnapshotId, UseDefMapBuilder, }; use crate::semantic_index::{ ExpressionsScopeMap, LoopHeader, LoopToken, SemanticIndex, VisibleAncestorsIter, @@ -845,12 +845,13 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } /// Create loop header definitions for all places that are bound within a loop. Return the - /// `LoopToken` referenced by those definitions, and the set of bound place IDs. + /// `LoopToken` referenced by those definitions, the set of bound place IDs, and the lower + /// bound `ScopedDefinitionId` for definitions created within the loop. fn synthesize_loop_header_definitions( &mut self, loop_stmt: LoopStmtRef<'ast>, bound_places: Vec, - ) -> (LoopToken<'db>, FxHashSet) { + ) -> (LoopToken<'db>, FxHashSet, ScopedDefinitionId) { let loop_token = LoopToken::new(self.db); let mut bound_place_ids: FxHashSet = FxHashSet::default(); for place_expr in bound_places { @@ -865,7 +866,8 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.push_additional_definition(place_id, loop_header_ref); } } - (loop_token, bound_place_ids) + let loop_min_definition_id = self.current_use_def_map_mut().next_definition_id(); + (loop_token, bound_place_ids, loop_min_definition_id) } /// Build a `LoopHeader` that tracks all the variables bound in a loop, which will be visible @@ -876,13 +878,18 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { &mut self, loop_header_places: &FxHashSet, loop_token: LoopToken<'db>, + loop_min_definition_id: ScopedDefinitionId, ) { let mut loop_header = LoopHeader::new(); let use_def = self.current_use_def_map_mut(); - // Collect bindings. + // Collect all the bindings within the loop that reached a loop back edge. Use the minimum + // definition ID to filter out all the pre-loop bindings. The loop header doesn't shadow + // them, so there's no need to duplicate them. for place_id in loop_header_places { for live_binding in use_def.loop_back_bindings(*place_id) { - loop_header.add_binding(*place_id, live_binding); + if live_binding.binding >= loop_min_definition_id { + loop_header.add_binding(*place_id, live_binding); + } } } // Mark the reachability and narrowing constraints as used. @@ -2225,8 +2232,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { // Collect all the loop-back bindings (including the `continue` states we just // merged) and populate the `LoopHeader`. - if let Some((loop_token, bound_place_ids)) = maybe_loop_header_info { - self.populate_loop_header(&bound_place_ids, loop_token); + if let Some((loop_token, bound_place_ids, loop_min_definition_id)) = + maybe_loop_header_info + { + self.populate_loop_header(&bound_place_ids, loop_token, loop_min_definition_id); } // We execute the `else` branch once the condition evaluates to false. This could @@ -2333,8 +2342,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { // Collect all the loop-back bindings (including the `continue` states we just // merged) and populate the `LoopHeader`. - if let Some((loop_token, bound_place_ids)) = maybe_loop_header_info { - self.populate_loop_header(&bound_place_ids, loop_token); + if let Some((loop_token, bound_place_ids, loop_min_definition_id)) = + maybe_loop_header_info + { + self.populate_loop_header(&bound_place_ids, loop_token, loop_min_definition_id); } // We may execute the `else` clause without ever executing the body, so merge in diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index 7ecea1ada2b3e..65d71ba332e97 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -990,6 +990,10 @@ impl<'db> UseDefMapBuilder<'db> { } } + pub(super) fn next_definition_id(&self) -> ScopedDefinitionId { + self.all_definitions.next_index() + } + pub(super) fn record_binding( &mut self, place: ScopedPlaceId, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 1145d434259df..88e53da9ebed1 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5218,11 +5218,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Infer the type for a loop header definition. /// - /// The loop header sees all bindings that loop-back, either by reaching the end of the loop - /// body or a `continue` statement. This can include bindings from before the loop too, though - /// that's technically redundant, since the loop header definition itself doesn't shadow those - /// bindings. See `struct LoopHeader` in the semantic index for more on how all this fits - /// together. + /// The loop header sees all the bindings that originate in the loop and are visible at a + /// loop-back edge (either the end of the loop body or a `continue` statement). See `struct + /// LoopHeader` in the semantic index for more on how all this fits together. fn infer_loop_header_definition( &mut self, loop_header_kind: &LoopHeaderDefinitionKind<'db>, From a91591e8e83bc8ec2020154b626d6fd8dfdb2b34 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 3 Mar 2026 15:21:46 +0530 Subject: [PATCH 176/261] [ty] Add mdtest suite for `typing.Concatenate` (#23554) ## Summary This PR adds a comprehensive test suite for `typing.Concatenate` in preparation for https://github.com/astral-sh/ty/issues/1535 and to help the review process in https://github.com/astral-sh/ruff/pull/23119. ## Test Plan Run mdtest. --- .../resources/mdtest/annotations/callable.md | 8 - .../mdtest/generics/pep695/concatenate.md | 574 ++++++++++++++++++ .../type_properties/is_assignable_to.md | 107 ++++ 3 files changed, 681 insertions(+), 8 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index 55d1377621b96..b499255a007cf 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -373,14 +373,6 @@ def _(c: Callable[Concatenate[int, str, ...], int]): reveal_type(c) # revealed: (...) -> int ``` -And, as one of the parameter types: - -```py -def _(c: Callable[[Concatenate[int, str, ...], int], int]): - # TODO: Should reveal the correct signature - reveal_type(c) # revealed: (...) -> int -``` - Other type expressions can be nested inside `Concatenate`: ```py diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md new file mode 100644 index 0000000000000..2fd64d8819c76 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md @@ -0,0 +1,574 @@ +# `typing.Concatenate` + +```toml +[environment] +python-version = "3.12" +``` + +## Basic usage in `Callable` + +`Concatenate` is valid as the first argument to `Callable`, with a `ParamSpec` or `...` as its final +element. + +### With `ParamSpec` + +```py +from typing import Callable, Concatenate + +def foo[**P, R](func: Callable[Concatenate[int, P], R]) -> Callable[Concatenate[int, P], R]: + # TODO: Should reveal `(int, /, *args: P@foo.args, **kwargs: P@foo.kwargs) -> R@foo` + reveal_type(func) # revealed: (...) -> R@foo + return func + +def f(x: int, y: str) -> bool: + return True + +result = foo(f) +# TODO: Should reveal `(int, /, y: str) -> bool` +reveal_type(result) # revealed: (...) -> bool +``` + +### With ellipsis + +```py +from typing import Callable, Concatenate + +def _(c: Callable[Concatenate[int, str, ...], bool]): + # TODO: Should reveal `(int, str, /, ...) -> bool` + reveal_type(c) # revealed: (...) -> bool +``` + +### Complex types inside `Concatenate` + +```py +from typing import Callable, Concatenate + +def _(c: Callable[Concatenate[int | str, list[int], type[str], ...], None]): + # TODO: Should reveal `(int | str, list[int], type[str], ...) -> None` + reveal_type(c) # revealed: (...) -> None +``` + +### Nested + +```py +from typing import Callable, Concatenate + +def _(c: Callable[Concatenate[int, Callable[Concatenate[str, ...], None], ...], None]): + # TODO: Should reveal `(int, (str, ...) -> None, /, ...) -> None` + reveal_type(c) # revealed: (...) -> None +``` + +## Decorator patterns + +### Adding a parameter + +A decorator that adds a parameter to the beginning of the callable's signature. + +```py +from typing import Callable, Concatenate + +def add_param[**P, R](func: Callable[P, R]) -> Callable[Concatenate[int, P], R]: + def wrapper(param: int, *args: P.args, **kwargs: P.kwargs) -> R: + return func(*args, **kwargs) + return wrapper + +@add_param +def f(x: str, y: bytes) -> int: + return 1 + +# TODO: Should reveal `(int, /, x: str, y: bytes) -> int` +reveal_type(f) # revealed: (...) -> int + +reveal_type(f(1, "", b"")) # revealed: int + +# TODO: This should be an error since `param` is a positional-only parameter +reveal_type(f(param=1, x="", y=b"")) # revealed: int +``` + +### Removing a parameter + +A decorator that removes the first parameter from the callable's signature. + +```py +from typing import Callable, Concatenate + +def remove_param[**P, R](func: Callable[Concatenate[int, P], R]) -> Callable[P, R]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + return func(0, *args, **kwargs) + # TODO: no error expected here + return wrapper # error: [invalid-return-type] + +@remove_param +def f(x: int, y: str, z: bytes) -> int: + return 1 + +# TODO: Should reveal `(y: str, z: bytes) -> int` +reveal_type(f) # revealed: [**P'return](**P'return) -> int + +# TODO: Shouldn't be an error +# error: [missing-argument] +reveal_type(f("", b"")) # revealed: int +# TODO: Shouldn't be an error +# error: [missing-argument] +reveal_type(f(y="", z=b"")) # revealed: int + +# TODO: missing-argument is an incorrect error, it should be [unknown-argument] since `x` is removed +# error: [missing-argument] "No argument provided for required parameter `*args`" +reveal_type(f(x=1, y="", z=b"")) # revealed: int +``` + +### Transforming a parameter + +A decorator that transforms the first parameter type. + +```py +from typing import Callable, Concatenate + +def transform[**P, R](func: Callable[Concatenate[int, P], R]) -> Callable[Concatenate[str, P], R]: + def wrapper(param: str, *args: P.args, **kwargs: P.kwargs) -> R: + return func(int(param), *args, **kwargs) + return wrapper + +@transform +def f(x: int, y: int) -> int: + return 1 + +# TODO: Should reveal `(str, /, y: int) -> int` +reveal_type(f) # revealed: (...) -> int + +reveal_type(f("", 1)) # revealed: int +reveal_type(f("", y=1)) # revealed: int + +# TODO: This should be an error since `param` is a positional-only parameter +reveal_type(f(param="", y=1)) # revealed: int +``` + +### Prepending multiple parameters + +```py +from typing import Callable, Concatenate + +def multi[**P, R](func: Callable[P, R]) -> Callable[Concatenate[int, str, P], R]: + def wrapper(a: int, b: str, *args: P.args, **kwargs: P.kwargs) -> R: + return func(*args, **kwargs) + return wrapper + +@multi +def f(x: int) -> int: + return 1 + +# TODO: Should reveal `(int, str, /, x: int) -> int` +reveal_type(f) # revealed: (...) -> int + +reveal_type(f(1, "", 2)) # revealed: int +reveal_type(f(1, "", x=2)) # revealed: int + +# TODO: This should be an error since `a` and `b` are positional-only parameters +reveal_type(f(a=1, b="", x=2)) # revealed: int +``` + +## Invalid uses of `Concatenate` + +### Standalone annotation (not inside `Callable`) + +`Concatenate` is only valid as the first argument to `Callable` or in the context of a `ParamSpec` +type argument. + +```py +from typing import Concatenate + +# error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression" +def _(x: Concatenate): ... + +# TODO: Should be an error - Concatenate is not a valid standalone type +def invalid1(x: Concatenate[int, ...]) -> None: ... + +# TODO: Should be an error - Concatenate is not a valid standalone type +def invalid2() -> Concatenate[int, ...]: ... +``` + +### Too few arguments + +```py +from typing import Callable, Concatenate + +def _( + # error: [invalid-type-form] "Special form `typing.Concatenate` expected at least 2 parameters but got 0" + a: Callable[Concatenate[()], int], + # error: [invalid-type-form] "Special form `typing.Concatenate` expected at least 2 parameters but got 1" + b: Callable[Concatenate[int], int], + # error: [invalid-type-form] "Special form `typing.Concatenate` expected at least 2 parameters but got 1" + c: Callable[Concatenate[(int,)], int], +): + reveal_type(a) # revealed: (...) -> int + reveal_type(b) # revealed: (...) -> int + reveal_type(c) # revealed: (...) -> int +``` + +### Last argument must be `ParamSpec` or `...` + +The final argument to `Concatenate` must be a `ParamSpec` or `...`. + +```py +from typing import Callable, Concatenate + +# TODO: Should be an error - last arg is not ParamSpec or `...` +def _(c: Callable[Concatenate[int, str], bool]): ... +``` + +### `ParamSpec` must be last + +If a `ParamSpec` appears in `Concatenate`, it must be the last element. + +```py +from typing import Callable, Concatenate + +# TODO: Should be an error - ParamSpec not in last position +def invalid1[**P](c: Callable[Concatenate[P, int], bool]): + reveal_type(c) # revealed: (...) -> bool + +# TODO: Should be an error - ParamSpec not in last position +def invalid2[**P](c: Callable[Concatenate[P, ...], bool]): + reveal_type(c) # revealed: (...) -> bool + +def valid[**P](c: Callable[Concatenate[int, P], bool]): + # TODO: Should reveal `(int, /, **P@valid) -> bool` + reveal_type(c) # revealed: (...) -> bool +``` + +### Nested `Concatenate` + +```py +from typing import Callable, Concatenate + +# TODO: This should be an error +def invalid[**P](c: Callable[Concatenate[Concatenate[int, ...], P], None]): + pass +``` + +## Specialization with concrete types + +When a `Callable[Concatenate[X, P], R]` is specialized with concrete arguments, `P` should be +inferred from the remaining parameters. + +```py +from typing import Callable, Concatenate + +def decorator[**P](func: Callable[Concatenate[int, P], bool]) -> Callable[P, bool]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> bool: + return func(0, *args, **kwargs) + # TODO: no error expected here + return wrapper # error: [invalid-return-type] + +# TODO: This should be an error because the required `int` parameter is missing +@decorator +def f0() -> bool: + return True + +@decorator +def f1(a: int) -> bool: + return True + +@decorator +def f2(a: int, b: str) -> bool: + return True + +# TODO: This call should be an error because the `str` is not assignable to `int` +@decorator +def f3(a: str, b: int) -> bool: + return True + +# TODO: Should reveal `() -> bool` +reveal_type(f1) # revealed: [**P'return](**P'return) -> bool +# TODO: Should reveal `(b: str) -> bool` +reveal_type(f2) # revealed: [**P'return](**P'return) -> bool +``` + +## Generic classes + +### In class attributes + +```py +from typing import Callable, Concatenate + +class Middleware[**P, R]: + handler: Callable[Concatenate[str, P], R] + + def __init__(self, handler: Callable[Concatenate[str, P], R]) -> None: + self.handler = handler + +def my_handler(env: str, x: int, y: float) -> bool: + return True + +m = Middleware(my_handler) +# TODO: Should reveal `Middleware[((x: int, y: float)), bool]` or similar +reveal_type(m) # revealed: Middleware[(...), bool] +``` + +### Specializing `ParamSpec` with `Concatenate` + +When explicitly specializing a generic class that takes a `ParamSpec`, a `Concatenate` form can be +provided as a type argument. + +```py +from typing import Callable, Concatenate + +class Foo[**P1]: + attr: Callable[P1, None] + +def with_paramspec[**P2](f: Foo[Concatenate[int, P2]]) -> None: + # TODO: Should reveal `Callable[Concatenate[int, P2], None]` + reveal_type(f.attr) # revealed: (...) -> None +``` + +## `Concatenate` in type aliases + +### Using `type` statement (PEP 695) + +```py +from typing import Callable, Concatenate + +type Foo[**P, R] = Callable[Concatenate[int, P], R] + +def _(f: Foo[[str], bool]) -> None: + # TODO: Should reveal `(int, str, /) -> bool` + reveal_type(f) # revealed: (...) -> bool +``` + +### Using `TypeAlias` + +```py +from typing import Callable, Concatenate, ParamSpec, TypeVar +from typing import TypeAlias + +P = ParamSpec("P") +R = TypeVar("R") + +Foo: TypeAlias = Callable[Concatenate[int, P], R] + +def _(f: Foo[[str], bool]) -> None: + # TODO: Should reveal `(int, str, /) -> bool` + reveal_type(f) # revealed: Unknown +``` + +## `Concatenate` with different parameter kinds + +### Function with keyword-only parameters after `Concatenate` prefix + +```py +from typing import Callable, Concatenate + +def decorator[**P](func: Callable[Concatenate[int, P], None]) -> Callable[P, None]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: + func(0, *args, **kwargs) + # TODO: no error expected here + return wrapper # error: [invalid-return-type] + +@decorator +def kwonly(x: int, *, key: str) -> None: ... + +# TODO: Should reveal `(*, key: str) -> None` +reveal_type(kwonly) # revealed: [**P'return](**P'return) -> None +``` + +### Function with default values + +```py +from typing import Callable, Concatenate + +def decorator[**P](func: Callable[Concatenate[int, P], None]) -> Callable[P, None]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: + func(0, *args, **kwargs) + # TODO: no error expected here + return wrapper # error: [invalid-return-type] + +@decorator +def defaults(x: int, y: str = "default", z: int = 0) -> None: ... + +# TODO: Should reveal `(y: str = "default", z: int = 0) -> None` +reveal_type(defaults) # revealed: [**P'return](**P'return) -> None +``` + +### Function with `*args` and `**kwargs` + +```py +from typing import Callable, Concatenate + +def decorator[**P](func: Callable[Concatenate[int, P], None]) -> Callable[P, None]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: + func(0, *args, **kwargs) + # TODO: no error expected here + return wrapper # error: [invalid-return-type] + +@decorator +def variadic(x: int, *args: str, **kwargs: int) -> None: ... + +# TODO: Should reveal `(*args: str, **kwargs: int) -> None` +reveal_type(variadic) # revealed: [**P'return](**P'return) -> None + +@decorator +def only_variadic(*args: str, **kwargs: int) -> None: ... + +# TODO: Should reveal `(*args: str, **kwargs: int) -> None` +reveal_type(only_variadic) # revealed: [**P'return](**P'return) -> None + +@decorator +def unpack_variadic(*args: *tuple[int, *tuple[str, ...]], **kwargs: int) -> None: ... + +# TODO: should reveal `(*args: str, **kwargs: int) -> None` +reveal_type(unpack_variadic) # revealed: [**P'return](**P'return) -> None +``` + +## `Concatenate` with `ParamSpec` in generic function calls + +### Basic call with inferred `ParamSpec` + +```py +from typing import Callable, Concatenate + +def foo[**P, R](func: Callable[Concatenate[int, P], R], *args: P.args, **kwargs: P.kwargs) -> R: + return func(0, *args, **kwargs) + +def test(x: str, y: str) -> bool: + return True + +reveal_type(foo(test, "", "")) # revealed: bool +reveal_type(foo(test, y="", x="")) # revealed: bool + +# TODO: These calls should raise an error +reveal_type(foo(test, 1, "")) # revealed: bool +reveal_type(foo(test, "")) # revealed: bool +``` + +### Prepended type variable + +```py +from typing import Callable, Concatenate + +def decorator[T, R, **P](func: Callable[Concatenate[T, P], R], *args: P.args, **kwargs: P.kwargs) -> Callable[[T], R]: + def wrapper(arg: T, /) -> R: + return func(arg, *args, **kwargs) + return wrapper + +@decorator +def test1(x: str, y: str) -> bool: + return True + +# TODO: should reveal (str, /) -> bool +reveal_type(test1) # revealed: [T'return](T'return, /) -> bool +reveal_type(test1("")) # revealed: bool +# error: [too-many-positional-arguments] +reveal_type(test1("", "")) # revealed: bool + +# TODO: This should be an error since a keyword-only parameter cannot be assigned to positional-only +# parameter `T` +@decorator +def test2(*, x: int) -> bool: + return True +``` + +## `Concatenate` with overloaded functions + +A function that accepts an overloaded callable via `Callable[Concatenate[int, P], R]` should be able +to strip the first parameter and infer `P` from the remaining overload signatures. + +```py +from typing import Callable, Concatenate, overload + +def remove_param[**P, R](func: Callable[Concatenate[int, P], R]) -> Callable[P, R]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + return func(0, *args, **kwargs) + # TODO: no error expected here + return wrapper # error: [invalid-return-type] + +@overload +def f1(x: int, y: str) -> str: ... +@overload +def f1(x: int, y: int) -> int: ... +@remove_param +def f1(x: int, y: str | int) -> str | int: + return y + +# TODO: Should reveal `Overloaded[(y: str) -> str, (y: int) -> int]` +reveal_type(f1) # revealed: [**P'return](**P'return) -> str | int +``` + +But, it's not possible to _add_ a parameter to an overloaded function using `Concatenate` because +the overload signatures don't have the extra parameter. + +```py +def add_param[**P, R](func: Callable[P, R]) -> Callable[Concatenate[int, P], R]: + def wrapper(param: int, *args: P.args, **kwargs: P.kwargs) -> R: + return func(*args, **kwargs) + return wrapper + +# TODO: Raise a diagnostic stating that the signature of the implementation doesn't match the +# overloads because the overloads don't have the extra `int` parameter. +@overload +def f2(y: str) -> str: ... +@overload +def f2(y: int) -> int: ... +@add_param +def f2(y: str | int) -> str | int: + return y + +# TODO: Should this reveal `Overloaded[(int, /, y: str) -> str, (int, /, y: int) -> int]` ? +reveal_type(f2) # revealed: (...) -> str | int +``` + +But, it's possible to add the additional parameter just to the overload signatures and not the +implementation: + +```py +@overload +def f3(x: int, /, y: str) -> str: ... +@overload +def f3(x: int, /, y: int) -> int: ... +@add_param +def f3(y: str | int) -> str | int: + return y + +# TODO: Should reveal `Overloaded[(int, /, y: str) -> str, (int, /, y: int) -> int]` +reveal_type(f3) # revealed: (...) -> str | int +``` + +## `Concatenate` with protocol classes + +A protocol with `ParamSpec` in its `__call__` can be used where `Callable[Concatenate[...], ...]` is +expected. + +```py +from typing import Protocol, Concatenate, Callable + +class Handler[**P, R](Protocol): + def __call__(self, value: int, *args: P.args, **kwargs: P.kwargs) -> R: ... + +def process[**P, R](handler: Handler[P, R], *args: P.args, **kwargs: P.kwargs) -> R: + return handler(0, *args, **kwargs) + +class MyHandler: + def __call__(self, value: int, name: str) -> bool: + return True + +# TODO: P should be inferred as [name: str], R as bool from MyHandler.__call__ +# TODO: These should not be errors +# TODO: Should reveal `bool` +# error: [invalid-argument-type] +reveal_type(process(MyHandler(), "hello")) # revealed: Unknown +# error: [invalid-argument-type] +reveal_type(process(MyHandler(), name="hello")) # revealed: Unknown + +def use_callable[**P, R](func: Callable[Concatenate[int, P], R], handler: Handler[P, R]) -> None: ... +``` + +## Importing from `typing_extensions` + +`Concatenate` should work the same whether imported from `typing` or `typing_extensions`. + +```py +from typing_extensions import Callable, Concatenate + +def _(c: Callable[Concatenate[int, str, ...], bool]): + # TODO: Should reveal `(int, str, ...) -> bool` + reveal_type(c) # revealed: (...) -> bool +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index d7135975f1c83..cb3755be1cde4 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1465,6 +1465,113 @@ def f(func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: static_assert(not is_assignable_to(dict[str, Unknown], TypeOf[kwargs])) ``` +## `Concatenate` + +### Self-assignability + +A `Callable` with `Concatenate` should be assignable to itself. + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Callable, Concatenate + +static_assert(is_assignable_to(Callable[Concatenate[int, ...], None], Callable[Concatenate[int, ...], None])) +static_assert(is_assignable_to(Callable[Concatenate[int, str, ...], None], Callable[Concatenate[int, str, ...], None])) +``` + +### Assignable to gradual callable + +A callable with `Concatenate` parameters should be assignable to the gradual callable form, since +the gradual form is consistent with any input signature. + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Callable, Concatenate + +static_assert(is_assignable_to(Callable[Concatenate[int, ...], None], Callable[..., None])) +static_assert(is_assignable_to(Callable[Concatenate[int, str, ...], None], Callable[..., None])) +``` + +And the gradual callable should also be assignable to one with `Concatenate` parameters. + +```py +static_assert(is_assignable_to(Callable[..., None], Callable[Concatenate[int, ...], None])) +static_assert(is_assignable_to(Callable[..., None], Callable[Concatenate[int, str, ...], None])) +``` + +### Contravariance of parameters + +Callable parameters are contravariant: a callable accepting a wider type (`A`) is assignable to one +expecting a narrower type (`B`), because any call valid for `B` is also valid for `A`. + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Callable, Concatenate + +class Parent: ... +class Child(Parent): ... + +static_assert(is_assignable_to(Callable[Concatenate[Parent, ...], None], Callable[Concatenate[Child, ...], None])) +# TODO: should not be assignable (`Parent` is not assignable to `Child`) +# error: [static-assert-error] +static_assert(not is_assignable_to(Callable[Concatenate[Child, ...], None], Callable[Concatenate[Parent, ...], None])) +``` + +### Different parameter types + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Callable, Concatenate, final + +class A: ... +class B: ... + +# TODO: should not be assignable (`A` and `B` are disjoint) +# error: [static-assert-error] +static_assert(not is_assignable_to(Callable[Concatenate[A, ...], None], Callable[Concatenate[B, ...], None])) +# TODO: should not be assignable +# error: [static-assert-error] +static_assert(not is_assignable_to(Callable[Concatenate[B, ...], None], Callable[Concatenate[A, ...], None])) +``` + +### Different number of prepended parameters + +Callables with different numbers of prepended parameters should be assignable. + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Callable, Concatenate + +static_assert(is_assignable_to(Callable[Concatenate[int, ...], None], Callable[Concatenate[int, str, ...], None])) +static_assert(is_assignable_to(Callable[Concatenate[int, str, ...], None], Callable[Concatenate[int, ...], None])) +``` + +### `Concatenate` with ellipsis vs explicit parameter list + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Callable, Concatenate + +static_assert(is_assignable_to(Callable[Concatenate[int, ...], None], Callable[[int], None])) +static_assert(is_assignable_to(Callable[[int], None], Callable[Concatenate[int, ...], None])) + +static_assert(is_assignable_to(Callable[Concatenate[int, ...], None], Callable[[int, str], None])) +static_assert(is_assignable_to(Callable[[int, str], None], Callable[Concatenate[int, ...], None])) +``` + +### `Concatenate` with `ParamSpec` + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Callable, Concatenate + +class A: ... + +def with_paramspec[**P](_: Callable[P, None]): + static_assert(is_assignable_to(Callable[Concatenate[int, P], None], Callable[..., None])) + static_assert(is_assignable_to(Callable[..., None], Callable[Concatenate[int, P], None])) +``` + [gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form [gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form [typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation From 4c360c6323b44dfc63812756a86ff5868b892d93 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 3 Mar 2026 11:04:46 +0000 Subject: [PATCH 177/261] Update conformance suite commit hash (#23693) --- .github/workflows/typing_conformance.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index d7aa4a7056926..f3ef000d07a2e 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -34,7 +34,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 RUST_BACKTRACE: 1 - CONFORMANCE_SUITE_COMMIT: e9fccc9dbbd8f1e8b24b4f88911c3d3155059e2a + CONFORMANCE_SUITE_COMMIT: 5b5f2f89bd19462f4707400f0437ab5a48d88bb3 PYTHON_VERSION: 3.12 jobs: From 9e69010d91cf171d2c6ad1ac0655fa9da62420fc Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 3 Mar 2026 12:09:30 +0000 Subject: [PATCH 178/261] [ty] Move `Type::subtyping_is_always_reflexive` to `types::relation` (#23692) --- crates/ty_python_semantic/src/types.rs | 47 ------------------- .../ty_python_semantic/src/types/relation.rs | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index eaaca8786181f..247bf6f711fd5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1835,53 +1835,6 @@ impl<'db> Type<'db> { } } - /// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for - /// any `T` of this type. - /// - /// This is true for fully static types, but also for some types that may not be fully static. - /// For example, a `ClassLiteral` may inherit `Any`, but its subtyping is still reflexive. - /// - /// This method may have false negatives, but it should not have false positives. It should be - /// a cheap shallow check, not an exhaustive recursive check. - const fn subtyping_is_always_reflexive(self) -> bool { - match self { - Type::Never - | Type::FunctionLiteral(..) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::KnownBoundMethod(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::ModuleLiteral(..) - | Type::LiteralValue(_) - | Type::SpecialForm(_) - | Type::KnownInstance(_) - | Type::AlwaysFalsy - | Type::AlwaysTruthy - | Type::PropertyInstance(_) - // `T` is always a subtype of itself, - // and `T` is always a subtype of `T | None` - | Type::TypeVar(_) - // might inherit `Any`, but subtyping is still reflexive - | Type::ClassLiteral(_) - => true, - Type::Dynamic(_) - | Type::NominalInstance(_) - | Type::ProtocolInstance(_) - | Type::GenericAlias(_) - | Type::SubclassOf(_) - | Type::Union(_) - | Type::Intersection(_) - | Type::Callable(_) - | Type::BoundSuper(_) - | Type::TypeIs(_) - | Type::TypeGuard(_) - | Type::TypedDict(_) - | Type::TypeAlias(_) - | Type::NewTypeInstance(_) => false, - } - } - pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { match self { Type::Callable(callable) => Some(CallableTypes::one(callable)), diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index f350ee1f874e8..952520b27fe21 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -226,6 +226,53 @@ impl TypeRelation { #[salsa::tracked] impl<'db> Type<'db> { + /// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for + /// any `T` of this type. + /// + /// This is true for fully static types, but also for some types that may not be fully static. + /// For example, a `ClassLiteral` may inherit `Any`, but its subtyping is still reflexive. + /// + /// This method may have false negatives, but it should not have false positives. It should be + /// a cheap shallow check, not an exhaustive recursive check. + const fn subtyping_is_always_reflexive(self) -> bool { + match self { + Type::Never + | Type::FunctionLiteral(..) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::KnownBoundMethod(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(..) + | Type::LiteralValue(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::PropertyInstance(_) + // `T` is always a subtype of itself, + // and `T` is always a subtype of `T | None` + | Type::TypeVar(_) + // might inherit `Any`, but subtyping is still reflexive + | Type::ClassLiteral(_) + => true, + Type::Dynamic(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::Union(_) + | Type::Intersection(_) + | Type::Callable(_) + | Type::BoundSuper(_) + | Type::TypeIs(_) + | Type::TypeGuard(_) + | Type::TypedDict(_) + | Type::TypeAlias(_) + | Type::NewTypeInstance(_) => false, + } + } + /// Return true if this type is a subtype of type `target`. /// /// See [`TypeRelation::Subtyping`] for more details. From bb80aff1be2a54a8c5fcb20693ccff279853d364 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 3 Mar 2026 12:14:34 +0000 Subject: [PATCH 179/261] [ty] Avoid the mandatory "ecosystem-analyzer workflow run cancelled" notification every time you make a PR (#23695) Co-authored-by: Claude --- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index d34246f646b4b..be2873309088a 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -14,7 +14,7 @@ on: concurrency: group: ty-ecosystem-analyzer-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ecosystem-analyzer') }} env: CARGO_INCREMENTAL: 0 From 7750704dec4865958912876cbe4e9b4445def7f6 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 3 Mar 2026 14:19:56 +0000 Subject: [PATCH 180/261] [ty] Move method-related types to a submodule (#23691) --- crates/ty_python_semantic/src/types.rs | 633 +---------------- crates/ty_python_semantic/src/types/method.rs | 646 ++++++++++++++++++ .../ty_python_semantic/src/types/visitor.rs | 8 +- 3 files changed, 653 insertions(+), 634 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/method.rs diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 247bf6f711fd5..f5c5aae538294 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1,5 +1,5 @@ use compact_str::ToCompactString; -use itertools::{Either, Itertools}; +use itertools::Itertools; use ruff_diagnostics::{Edit, Fix}; use rustc_hash::FxHashMap; @@ -68,6 +68,7 @@ use crate::types::generics::{ }; pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; use crate::types::known_instance::{InternedConstraintSet, InternedType, UnionTypeInstance}; +pub use crate::types::method::{BoundMethodType, KnownBoundMethodType, WrapperDescriptorKind}; use crate::types::mro::{Mro, MroIterator, StaticMroError}; pub(crate) use crate::types::narrow::{ NarrowingConstraint, PossiblyNarrowedPlaces, PossiblyNarrowedPlacesBuilder, @@ -114,6 +115,7 @@ mod known_instance; pub mod list_members; mod literal; mod member; +mod method; mod mro; mod narrow; mod newtype; @@ -8194,133 +8196,6 @@ impl From for Truthiness { } } -/// This type represents bound method objects that are created when a method is accessed -/// on an instance of a class. For example, the expression `Path("a.txt").touch` creates -/// a bound method object that represents the `Path.touch` method which is bound to the -/// instance `Path("a.txt")`. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct BoundMethodType<'db> { - /// The function that is being bound. Corresponds to the `__func__` attribute on a - /// bound method object - pub(crate) function: FunctionType<'db>, - /// The instance on which this method has been called. Corresponds to the `__self__` - /// attribute on a bound method object - self_instance: Type<'db>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for BoundMethodType<'_> {} - -fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - method: BoundMethodType<'db>, - visitor: &V, -) { - visitor.visit_function_type(db, method.function(db)); - visitor.visit_type(db, method.self_instance(db)); -} - -fn into_callable_type_cycle_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - _self: BoundMethodType<'db>, -) -> CallableType<'db> { - CallableType::bottom(db) -} - -#[salsa::tracked] -impl<'db> BoundMethodType<'db> { - /// Returns the type that replaces any `typing.Self` annotations in the bound method signature. - /// This is normally the bound-instance type (the type of `self` or `cls`), but if the bound method is - /// a `@classmethod`, then it should be an instance of that bound-instance type. - pub(crate) fn typing_self_type(self, db: &'db dyn Db) -> Type<'db> { - let mut self_instance = self.self_instance(db); - if self.function(db).is_classmethod(db) { - self_instance = self_instance.to_instance(db).unwrap_or_else(Type::unknown); - } - self_instance - } - - pub(crate) fn map_self_type( - self, - db: &'db dyn Db, - f: impl FnOnce(Type<'db>) -> Type<'db>, - ) -> Self { - Self::new(db, self.function(db), f(self.self_instance(db))) - } - - #[salsa::tracked(cycle_initial=into_callable_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { - let function = self.function(db); - let self_instance = self.typing_self_type(db); - - CallableType::new( - db, - CallableSignature::from_overloads( - function - .signature(db) - .overloads - .iter() - .map(|signature| signature.bind_self(db, Some(self_instance))), - ), - CallableTypeKind::FunctionLike, - ) - } - - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - Some(Self::new( - db, - self.function(db) - .recursive_type_normalized_impl(db, div, nested)?, - self.self_instance(db) - .recursive_type_normalized_impl(db, div, true)?, - )) - } - - #[expect(clippy::too_many_arguments)] - fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - // A bound method is a typically a subtype of itself. However, we must explicitly verify - // the subtyping of the underlying function signatures (since they might be specialized - // differently), and of the bound self parameter (taking care that parameters, including a - // bound self parameter, are contravariant.) - self.function(db) - .has_relation_to_impl( - db, - other.function(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - .and(db, constraints, || { - other.self_instance(db).has_relation_to_impl( - db, - self.self_instance(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum CallableTypeKind { /// Represents regular callable objects. @@ -8613,508 +8488,6 @@ impl<'db> CallableTypes<'db> { } } -/// Represents a specific instance of a bound method type for a builtin class. -/// -/// Unlike bound methods of user-defined classes, these are not generally instances -/// of `types.BoundMethodType` at runtime. -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub enum KnownBoundMethodType<'db> { - /// Method wrapper for `some_function.__get__` - FunctionTypeDunderGet(FunctionType<'db>), - /// Method wrapper for `some_function.__call__` - FunctionTypeDunderCall(FunctionType<'db>), - /// Method wrapper for `some_property.__get__` - PropertyDunderGet(PropertyInstanceType<'db>), - /// Method wrapper for `some_property.__set__` - PropertyDunderSet(PropertyInstanceType<'db>), - /// Method wrapper for `str.startswith`. - /// We treat this method specially because we want to be able to infer precise Boolean - /// literal return types if the instance and the prefix are both string literals, and - /// this allows us to understand statically known branches for common tests such as - /// `if sys.platform.startswith("freebsd")`. - StrStartswith(StringLiteralType<'db>), - - // ConstraintSet methods - ConstraintSetRange, - ConstraintSetAlways, - ConstraintSetNever, - ConstraintSetImpliesSubtypeOf(InternedConstraintSet<'db>), - ConstraintSetSatisfies(InternedConstraintSet<'db>), - ConstraintSetSatisfiedByAllTypeVars(InternedConstraintSet<'db>), -} - -pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - method_wrapper: KnownBoundMethodType<'db>, - visitor: &V, -) { - match method_wrapper { - KnownBoundMethodType::FunctionTypeDunderGet(function) => { - visitor.visit_function_type(db, function); - } - KnownBoundMethodType::FunctionTypeDunderCall(function) => { - visitor.visit_function_type(db, function); - } - KnownBoundMethodType::PropertyDunderGet(property) => { - visitor.visit_property_instance_type(db, property); - } - KnownBoundMethodType::PropertyDunderSet(property) => { - visitor.visit_property_instance_type(db, property); - } - KnownBoundMethodType::StrStartswith(string_literal) => { - visitor.visit_type( - db, - LiteralValueType::promotable(LiteralValueTypeKind::String(string_literal)).into(), - ); - } - KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {} - } -} - -impl<'db> KnownBoundMethodType<'db> { - #[expect(clippy::too_many_arguments)] - fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match (self, other) { - ( - KnownBoundMethodType::FunctionTypeDunderGet(self_function), - KnownBoundMethodType::FunctionTypeDunderGet(other_function), - ) => self_function.has_relation_to_impl( - db, - other_function, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - - ( - KnownBoundMethodType::FunctionTypeDunderCall(self_function), - KnownBoundMethodType::FunctionTypeDunderCall(other_function), - ) => self_function.has_relation_to_impl( - db, - other_function, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - - ( - KnownBoundMethodType::PropertyDunderGet(self_property), - KnownBoundMethodType::PropertyDunderGet(other_property), - ) - | ( - KnownBoundMethodType::PropertyDunderSet(self_property), - KnownBoundMethodType::PropertyDunderSet(other_property), - ) => Type::PropertyInstance(self_property).when_equivalent_to_impl( - db, - Type::PropertyInstance(other_property), - constraints, - relation_visitor, - disjointness_visitor, - ), - - (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { - ConstraintSet::from_bool(constraints, self == other) - } - - ( - KnownBoundMethodType::ConstraintSetRange, - KnownBoundMethodType::ConstraintSetRange, - ) - | ( - KnownBoundMethodType::ConstraintSetAlways, - KnownBoundMethodType::ConstraintSetAlways, - ) - | ( - KnownBoundMethodType::ConstraintSetNever, - KnownBoundMethodType::ConstraintSetNever, - ) - | ( - KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), - KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), - ) - | ( - KnownBoundMethodType::ConstraintSetSatisfies(_), - KnownBoundMethodType::ConstraintSetSatisfies(_), - ) - | ( - KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - ) => ConstraintSet::from_bool(constraints, true), - - ( - KnownBoundMethodType::FunctionTypeDunderGet(_) - | KnownBoundMethodType::FunctionTypeDunderCall(_) - | KnownBoundMethodType::PropertyDunderGet(_) - | KnownBoundMethodType::PropertyDunderSet(_) - | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - KnownBoundMethodType::FunctionTypeDunderGet(_) - | KnownBoundMethodType::FunctionTypeDunderCall(_) - | KnownBoundMethodType::PropertyDunderGet(_) - | KnownBoundMethodType::PropertyDunderSet(_) - | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - ) => ConstraintSet::from_bool(constraints, false), - } - } - - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - match self { - KnownBoundMethodType::FunctionTypeDunderGet(function) => { - Some(KnownBoundMethodType::FunctionTypeDunderGet( - function.recursive_type_normalized_impl(db, div, nested)?, - )) - } - KnownBoundMethodType::FunctionTypeDunderCall(function) => { - Some(KnownBoundMethodType::FunctionTypeDunderCall( - function.recursive_type_normalized_impl(db, div, nested)?, - )) - } - KnownBoundMethodType::PropertyDunderGet(property) => { - Some(KnownBoundMethodType::PropertyDunderGet( - property.recursive_type_normalized_impl(db, div, nested)?, - )) - } - KnownBoundMethodType::PropertyDunderSet(property) => { - Some(KnownBoundMethodType::PropertyDunderSet( - property.recursive_type_normalized_impl(db, div, nested)?, - )) - } - KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => Some(self), - } - } - - /// Return the [`KnownClass`] that inhabitants of this type are instances of at runtime - fn class(self) -> KnownClass { - match self { - KnownBoundMethodType::FunctionTypeDunderGet(_) - | KnownBoundMethodType::FunctionTypeDunderCall(_) - | KnownBoundMethodType::PropertyDunderGet(_) - | KnownBoundMethodType::PropertyDunderSet(_) => KnownClass::MethodWrapperType, - KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType, - KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { - KnownClass::ConstraintSet - } - } - } - - /// Return the signatures of this bound method type. - /// - /// If the bound method type is overloaded, it may have multiple signatures. - fn signatures(self, db: &'db dyn Db) -> impl Iterator> { - match self { - // Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`. - // This is required because we need to return more precise types than what the signature in - // typeshed provides: - // - // ```py - // class FunctionType: - // # ... - // @overload - // def __get__(self, instance: None, owner: type, /) -> FunctionType: ... - // @overload - // def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... - // ``` - // - // For `builtins.property.__get__`, we use the same signature. The return types are not - // specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`. - // - // TODO: Consider merging these synthesized signatures with the ones in - // [`WrapperDescriptorKind::signatures`], since this one is just that signature - // with the `self` parameters removed. - KnownBoundMethodType::FunctionTypeDunderGet(_) - | KnownBoundMethodType::PropertyDunderGet(_) => Either::Left(Either::Left( - [ - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::none(db)), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(KnownClass::Type.to_instance(db)), - ], - ), - Type::unknown(), - ), - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::object()), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(UnionType::from_two_elements( - db, - KnownClass::Type.to_instance(db), - Type::none(db), - )) - .with_default_type(Type::none(db)), - ], - ), - Type::unknown(), - ), - ] - .into_iter(), - )), - KnownBoundMethodType::FunctionTypeDunderCall(function) => Either::Left(Either::Right( - function.signature(db).overloads.iter().cloned(), - )), - KnownBoundMethodType::PropertyDunderSet(_) => { - Either::Right(std::iter::once(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::object()), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::object()), - ], - ), - Type::unknown(), - ))) - } - KnownBoundMethodType::StrStartswith(_) => { - Either::Right(std::iter::once(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("prefix"))) - .with_annotated_type(UnionType::from_two_elements( - db, - KnownClass::Str.to_instance(db), - Type::homogeneous_tuple(db, KnownClass::Str.to_instance(db)), - )), - Parameter::positional_only(Some(Name::new_static("start"))) - .with_annotated_type(UnionType::from_two_elements( - db, - KnownClass::SupportsIndex.to_instance(db), - Type::none(db), - )) - .with_default_type(Type::none(db)), - Parameter::positional_only(Some(Name::new_static("end"))) - .with_annotated_type(UnionType::from_two_elements( - db, - KnownClass::SupportsIndex.to_instance(db), - Type::none(db), - )) - .with_default_type(Type::none(db)), - ], - ), - KnownClass::Bool.to_instance(db), - ))) - } - - KnownBoundMethodType::ConstraintSetRange => { - Either::Right(std::iter::once(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("lower_bound"))) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("typevar"))) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("upper_bound"))) - .type_form() - .with_annotated_type(Type::any()), - ], - ), - KnownClass::ConstraintSet.to_instance(db), - ))) - } - - KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever => { - Either::Right(std::iter::once(Signature::new( - Parameters::empty(), - KnownClass::ConstraintSet.to_instance(db), - ))) - } - - KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => { - Either::Right(std::iter::once(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("ty"))) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("of"))) - .type_form() - .with_annotated_type(Type::any()), - ], - ), - KnownClass::ConstraintSet.to_instance(db), - ))) - } - - KnownBoundMethodType::ConstraintSetSatisfies(_) => { - Either::Right(std::iter::once(Signature::new( - Parameters::new( - db, - [Parameter::positional_only(Some(Name::new_static("other"))) - .with_annotated_type(KnownClass::ConstraintSet.to_instance(db))], - ), - KnownClass::ConstraintSet.to_instance(db), - ))) - } - - KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { - Either::Right(std::iter::once(Signature::new( - Parameters::new( - db, - [Parameter::keyword_only(Name::new_static("inferable")) - .type_form() - .with_annotated_type(UnionType::from_two_elements( - db, - Type::homogeneous_tuple(db, Type::any()), - Type::none(db), - )) - .with_default_type(Type::none(db))], - ), - KnownClass::Bool.to_instance(db), - ))) - } - } - } -} - -/// Represents a specific instance of `types.WrapperDescriptorType` -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub enum WrapperDescriptorKind { - /// `FunctionType.__get__` - FunctionTypeDunderGet, - /// `property.__get__` - PropertyDunderGet, - /// `property.__set__` - PropertyDunderSet, -} - -impl WrapperDescriptorKind { - fn signatures(self, db: &dyn Db) -> impl Iterator> { - /// Similar to what we do in [`KnownBoundMethod::signatures`], - /// here we also model `types.FunctionType.__get__` (or builtins.property.__get__), - /// but now we consider a call to this as a function, i.e. we also expect the `self` - /// argument to be passed in. - /// - /// TODO: Consider merging these synthesized signatures with the ones in - /// [`KnownBoundMethod::signatures`], since that one is just this signature - /// with the `self` parameters removed. - fn dunder_get_signatures(db: &dyn Db, class: KnownClass) -> [Signature<'_>; 2] { - let type_instance = KnownClass::Type.to_instance(db); - let none = Type::none(db); - let descriptor = class.to_instance(db); - [ - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(descriptor), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(none), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(type_instance), - ], - ), - Type::unknown(), - ), - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(descriptor), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::object()), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(UnionType::from_two_elements( - db, - type_instance, - none, - )) - .with_default_type(none), - ], - ), - Type::unknown(), - ), - ] - } - - match self { - WrapperDescriptorKind::FunctionTypeDunderGet => { - Either::Left(dunder_get_signatures(db, KnownClass::FunctionType).into_iter()) - } - WrapperDescriptorKind::PropertyDunderGet => { - Either::Left(dunder_get_signatures(db, KnownClass::Property).into_iter()) - } - WrapperDescriptorKind::PropertyDunderSet => { - let object = Type::object(); - Either::Right(std::iter::once(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::Property.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(object), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(object), - ], - ), - Type::unknown(), - ))) - } - } - } -} - #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct ModuleLiteralType<'db> { /// The imported module. diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs new file mode 100644 index 0000000000000..99510ecab080c --- /dev/null +++ b/crates/ty_python_semantic/src/types/method.rs @@ -0,0 +1,646 @@ +use itertools::Either; +use ruff_python_ast::name::Name; + +use crate::{ + Db, + types::{ + CallableSignature, CallableType, CallableTypeKind, KnownClass, LiteralValueType, + LiteralValueTypeKind, Parameter, Parameters, PropertyInstanceType, Signature, + StringLiteralType, Type, UnionType, + constraints::{ConstraintSet, ConstraintSetBuilder}, + function::FunctionType, + generics::InferableTypeVars, + known_instance::InternedConstraintSet, + relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}, + visitor, + }, +}; + +/// This type represents bound method objects that are created when a method is accessed +/// on an instance of a class. For example, the expression `Path("a.txt").touch` creates +/// a bound method object that represents the `Path.touch` method which is bound to the +/// instance `Path("a.txt")`. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct BoundMethodType<'db> { + /// The function that is being bound. Corresponds to the `__func__` attribute on a + /// bound method object + pub(crate) function: FunctionType<'db>, + /// The instance on which this method has been called. Corresponds to the `__self__` + /// attribute on a bound method object + pub(super) self_instance: Type<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for BoundMethodType<'_> {} + +pub(super) fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + method: BoundMethodType<'db>, + visitor: &V, +) { + visitor.visit_function_type(db, method.function(db)); + visitor.visit_type(db, method.self_instance(db)); +} + +fn into_callable_type_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + _self: BoundMethodType<'db>, +) -> CallableType<'db> { + CallableType::bottom(db) +} + +#[salsa::tracked] +impl<'db> BoundMethodType<'db> { + /// Returns the type that replaces any `typing.Self` annotations in the bound method signature. + /// This is normally the bound-instance type (the type of `self` or `cls`), but if the bound method is + /// a `@classmethod`, then it should be an instance of that bound-instance type. + pub(crate) fn typing_self_type(self, db: &'db dyn Db) -> Type<'db> { + let mut self_instance = self.self_instance(db); + if self.function(db).is_classmethod(db) { + self_instance = self_instance.to_instance(db).unwrap_or_else(Type::unknown); + } + self_instance + } + + pub(crate) fn map_self_type( + self, + db: &'db dyn Db, + f: impl FnOnce(Type<'db>) -> Type<'db>, + ) -> Self { + Self::new(db, self.function(db), f(self.self_instance(db))) + } + + #[salsa::tracked(cycle_initial=into_callable_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { + let function = self.function(db); + let self_instance = self.typing_self_type(db); + + CallableType::new( + db, + CallableSignature::from_overloads( + function + .signature(db) + .overloads + .iter() + .map(|signature| signature.bind_self(db, Some(self_instance))), + ), + CallableTypeKind::FunctionLike, + ) + } + + pub(super) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + Some(Self::new( + db, + self.function(db) + .recursive_type_normalized_impl(db, div, nested)?, + self.self_instance(db) + .recursive_type_normalized_impl(db, div, true)?, + )) + } + + #[expect(clippy::too_many_arguments)] + pub(super) fn has_relation_to_impl<'c>( + self, + db: &'db dyn Db, + other: Self, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + // A bound method is a typically a subtype of itself. However, we must explicitly verify + // the subtyping of the underlying function signatures (since they might be specialized + // differently), and of the bound self parameter (taking care that parameters, including a + // bound self parameter, are contravariant.) + self.function(db) + .has_relation_to_impl( + db, + other.function(db), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + .and(db, constraints, || { + other.self_instance(db).has_relation_to_impl( + db, + self.self_instance(db), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } +} + +/// Represents a specific instance of a bound method type for a builtin class. +/// +/// Unlike bound methods of user-defined classes, these are not generally instances +/// of `types.BoundMethodType` at runtime. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub enum KnownBoundMethodType<'db> { + /// Method wrapper for `some_function.__get__` + FunctionTypeDunderGet(FunctionType<'db>), + /// Method wrapper for `some_function.__call__` + FunctionTypeDunderCall(FunctionType<'db>), + /// Method wrapper for `some_property.__get__` + PropertyDunderGet(PropertyInstanceType<'db>), + /// Method wrapper for `some_property.__set__` + PropertyDunderSet(PropertyInstanceType<'db>), + /// Method wrapper for `str.startswith`. + /// We treat this method specially because we want to be able to infer precise Boolean + /// literal return types if the instance and the prefix are both string literals, and + /// this allows us to understand statically known branches for common tests such as + /// `if sys.platform.startswith("freebsd")`. + StrStartswith(StringLiteralType<'db>), + + // ConstraintSet methods + ConstraintSetRange, + ConstraintSetAlways, + ConstraintSetNever, + ConstraintSetImpliesSubtypeOf(InternedConstraintSet<'db>), + ConstraintSetSatisfies(InternedConstraintSet<'db>), + ConstraintSetSatisfiedByAllTypeVars(InternedConstraintSet<'db>), +} + +pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + method_wrapper: KnownBoundMethodType<'db>, + visitor: &V, +) { + match method_wrapper { + KnownBoundMethodType::FunctionTypeDunderGet(function) => { + visitor.visit_function_type(db, function); + } + KnownBoundMethodType::FunctionTypeDunderCall(function) => { + visitor.visit_function_type(db, function); + } + KnownBoundMethodType::PropertyDunderGet(property) => { + visitor.visit_property_instance_type(db, property); + } + KnownBoundMethodType::PropertyDunderSet(property) => { + visitor.visit_property_instance_type(db, property); + } + KnownBoundMethodType::StrStartswith(string_literal) => { + visitor.visit_type( + db, + LiteralValueType::promotable(LiteralValueTypeKind::String(string_literal)).into(), + ); + } + KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {} + } +} + +impl<'db> KnownBoundMethodType<'db> { + #[expect(clippy::too_many_arguments)] + pub(super) fn has_relation_to_impl<'c>( + self, + db: &'db dyn Db, + other: Self, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + match (self, other) { + ( + KnownBoundMethodType::FunctionTypeDunderGet(self_function), + KnownBoundMethodType::FunctionTypeDunderGet(other_function), + ) => self_function.has_relation_to_impl( + db, + other_function, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ), + + ( + KnownBoundMethodType::FunctionTypeDunderCall(self_function), + KnownBoundMethodType::FunctionTypeDunderCall(other_function), + ) => self_function.has_relation_to_impl( + db, + other_function, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ), + + ( + KnownBoundMethodType::PropertyDunderGet(self_property), + KnownBoundMethodType::PropertyDunderGet(other_property), + ) + | ( + KnownBoundMethodType::PropertyDunderSet(self_property), + KnownBoundMethodType::PropertyDunderSet(other_property), + ) => Type::PropertyInstance(self_property).when_equivalent_to_impl( + db, + Type::PropertyInstance(other_property), + constraints, + relation_visitor, + disjointness_visitor, + ), + + (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { + ConstraintSet::from_bool(constraints, self == other) + } + + ( + KnownBoundMethodType::ConstraintSetRange, + KnownBoundMethodType::ConstraintSetRange, + ) + | ( + KnownBoundMethodType::ConstraintSetAlways, + KnownBoundMethodType::ConstraintSetAlways, + ) + | ( + KnownBoundMethodType::ConstraintSetNever, + KnownBoundMethodType::ConstraintSetNever, + ) + | ( + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + ) + | ( + KnownBoundMethodType::ConstraintSetSatisfies(_), + KnownBoundMethodType::ConstraintSetSatisfies(_), + ) + | ( + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + ) => ConstraintSet::from_bool(constraints, true), + + ( + KnownBoundMethodType::FunctionTypeDunderGet(_) + | KnownBoundMethodType::FunctionTypeDunderCall(_) + | KnownBoundMethodType::PropertyDunderGet(_) + | KnownBoundMethodType::PropertyDunderSet(_) + | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + KnownBoundMethodType::FunctionTypeDunderGet(_) + | KnownBoundMethodType::FunctionTypeDunderCall(_) + | KnownBoundMethodType::PropertyDunderGet(_) + | KnownBoundMethodType::PropertyDunderSet(_) + | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + ) => ConstraintSet::from_bool(constraints, false), + } + } + + pub(super) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + match self { + KnownBoundMethodType::FunctionTypeDunderGet(function) => { + Some(KnownBoundMethodType::FunctionTypeDunderGet( + function.recursive_type_normalized_impl(db, div, nested)?, + )) + } + KnownBoundMethodType::FunctionTypeDunderCall(function) => { + Some(KnownBoundMethodType::FunctionTypeDunderCall( + function.recursive_type_normalized_impl(db, div, nested)?, + )) + } + KnownBoundMethodType::PropertyDunderGet(property) => { + Some(KnownBoundMethodType::PropertyDunderGet( + property.recursive_type_normalized_impl(db, div, nested)?, + )) + } + KnownBoundMethodType::PropertyDunderSet(property) => { + Some(KnownBoundMethodType::PropertyDunderSet( + property.recursive_type_normalized_impl(db, div, nested)?, + )) + } + KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => Some(self), + } + } + + /// Return the [`KnownClass`] that inhabitants of this type are instances of at runtime + pub(super) fn class(self) -> KnownClass { + match self { + KnownBoundMethodType::FunctionTypeDunderGet(_) + | KnownBoundMethodType::FunctionTypeDunderCall(_) + | KnownBoundMethodType::PropertyDunderGet(_) + | KnownBoundMethodType::PropertyDunderSet(_) => KnownClass::MethodWrapperType, + KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType, + KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { + KnownClass::ConstraintSet + } + } + } + + /// Return the signatures of this bound method type. + /// + /// If the bound method type is overloaded, it may have multiple signatures. + pub(super) fn signatures(self, db: &'db dyn Db) -> impl Iterator> { + match self { + // Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`. + // This is required because we need to return more precise types than what the signature in + // typeshed provides: + // + // ```py + // class FunctionType: + // # ... + // @overload + // def __get__(self, instance: None, owner: type, /) -> FunctionType: ... + // @overload + // def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... + // ``` + // + // For `builtins.property.__get__`, we use the same signature. The return types are not + // specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`. + // + // TODO: Consider merging these synthesized signatures with the ones in + // [`WrapperDescriptorKind::signatures`], since this one is just that signature + // with the `self` parameters removed. + KnownBoundMethodType::FunctionTypeDunderGet(_) + | KnownBoundMethodType::PropertyDunderGet(_) => Either::Left(Either::Left( + [ + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::none(db)), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(KnownClass::Type.to_instance(db)), + ], + ), + Type::unknown(), + ), + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object()), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(UnionType::from_two_elements( + db, + KnownClass::Type.to_instance(db), + Type::none(db), + )) + .with_default_type(Type::none(db)), + ], + ), + Type::unknown(), + ), + ] + .into_iter(), + )), + KnownBoundMethodType::FunctionTypeDunderCall(function) => Either::Left(Either::Right( + function.signature(db).overloads.iter().cloned(), + )), + KnownBoundMethodType::PropertyDunderSet(_) => { + Either::Right(std::iter::once(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object()), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::object()), + ], + ), + Type::unknown(), + ))) + } + KnownBoundMethodType::StrStartswith(_) => { + Either::Right(std::iter::once(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("prefix"))) + .with_annotated_type(UnionType::from_two_elements( + db, + KnownClass::Str.to_instance(db), + Type::homogeneous_tuple(db, KnownClass::Str.to_instance(db)), + )), + Parameter::positional_only(Some(Name::new_static("start"))) + .with_annotated_type(UnionType::from_two_elements( + db, + KnownClass::SupportsIndex.to_instance(db), + Type::none(db), + )) + .with_default_type(Type::none(db)), + Parameter::positional_only(Some(Name::new_static("end"))) + .with_annotated_type(UnionType::from_two_elements( + db, + KnownClass::SupportsIndex.to_instance(db), + Type::none(db), + )) + .with_default_type(Type::none(db)), + ], + ), + KnownClass::Bool.to_instance(db), + ))) + } + + KnownBoundMethodType::ConstraintSetRange => { + Either::Right(std::iter::once(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("lower_bound"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("typevar"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("upper_bound"))) + .type_form() + .with_annotated_type(Type::any()), + ], + ), + KnownClass::ConstraintSet.to_instance(db), + ))) + } + + KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever => { + Either::Right(std::iter::once(Signature::new( + Parameters::empty(), + KnownClass::ConstraintSet.to_instance(db), + ))) + } + + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => { + Either::Right(std::iter::once(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("ty"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("of"))) + .type_form() + .with_annotated_type(Type::any()), + ], + ), + KnownClass::ConstraintSet.to_instance(db), + ))) + } + + KnownBoundMethodType::ConstraintSetSatisfies(_) => { + Either::Right(std::iter::once(Signature::new( + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("other"))) + .with_annotated_type(KnownClass::ConstraintSet.to_instance(db))], + ), + KnownClass::ConstraintSet.to_instance(db), + ))) + } + + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { + Either::Right(std::iter::once(Signature::new( + Parameters::new( + db, + [Parameter::keyword_only(Name::new_static("inferable")) + .type_form() + .with_annotated_type(UnionType::from_two_elements( + db, + Type::homogeneous_tuple(db, Type::any()), + Type::none(db), + )) + .with_default_type(Type::none(db))], + ), + KnownClass::Bool.to_instance(db), + ))) + } + } + } +} + +/// Represents a specific instance of `types.WrapperDescriptorType` +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub enum WrapperDescriptorKind { + /// `FunctionType.__get__` + FunctionTypeDunderGet, + /// `property.__get__` + PropertyDunderGet, + /// `property.__set__` + PropertyDunderSet, +} + +impl WrapperDescriptorKind { + pub(super) fn signatures(self, db: &dyn Db) -> impl Iterator> { + /// Similar to what we do in [`KnownBoundMethod::signatures`], + /// here we also model `types.FunctionType.__get__` (or builtins.property.__get__), + /// but now we consider a call to this as a function, i.e. we also expect the `self` + /// argument to be passed in. + /// + /// TODO: Consider merging these synthesized signatures with the ones in + /// [`KnownBoundMethod::signatures`], since that one is just this signature + /// with the `self` parameters removed. + fn dunder_get_signatures(db: &dyn Db, class: KnownClass) -> [Signature<'_>; 2] { + let type_instance = KnownClass::Type.to_instance(db); + let none = Type::none(db); + let descriptor = class.to_instance(db); + [ + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(descriptor), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(none), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(type_instance), + ], + ), + Type::unknown(), + ), + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(descriptor), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object()), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(UnionType::from_two_elements( + db, + type_instance, + none, + )) + .with_default_type(none), + ], + ), + Type::unknown(), + ), + ] + } + + match self { + WrapperDescriptorKind::FunctionTypeDunderGet => { + Either::Left(dunder_get_signatures(db, KnownClass::FunctionType).into_iter()) + } + WrapperDescriptorKind::PropertyDunderGet => { + Either::Left(dunder_get_signatures(db, KnownClass::Property).into_iter()) + } + WrapperDescriptorKind::PropertyDunderSet => { + let object = Type::object(); + Either::Right(std::iter::once(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(KnownClass::Property.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(object), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(object), + ], + ), + Type::unknown(), + ))) + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 002a81e828ef1..50847a0eb9304 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -12,12 +12,12 @@ use crate::{ function::{FunctionType, walk_function_type}, instance::{walk_nominal_instance_type, walk_protocol_instance_type}, known_instance::walk_known_instance_type, + method::{walk_bound_method_type, walk_method_wrapper_type}, newtype::{NewType, walk_newtype_instance_type}, subclass_of::walk_subclass_of_type, - walk_bound_method_type, walk_bound_type_var_type, walk_callable_type, - walk_intersection_type, walk_method_wrapper_type, walk_property_instance_type, - walk_type_alias_type, walk_type_var_type, walk_typed_dict_type, walk_typeguard_type, - walk_typeis_type, walk_union, + walk_bound_type_var_type, walk_callable_type, walk_intersection_type, + walk_property_instance_type, walk_type_alias_type, walk_type_var_type, + walk_typed_dict_type, walk_typeguard_type, walk_typeis_type, walk_union, }, }; use std::cell::{Cell, RefCell}; From 189361eb2ed72696843ef9454eb5e19d1831bc0b Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 3 Mar 2026 08:13:54 -0800 Subject: [PATCH 181/261] [`pydocstyle`] Fix numpy section ordering (`D420`) (#23685) --- .../test/fixtures/pydocstyle/D420_numpy.py | 16 ++++++++-------- .../src/rules/pydocstyle/rules/sections.rs | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_numpy.py index 4ef310ece57c3..c0c6b90bf0b72 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D420_numpy.py @@ -13,6 +13,14 @@ def correct_order(): x : int Description. + Attributes + ---------- + attr : int + + Methods + ------- + method + Returns ------- int @@ -57,14 +65,6 @@ def correct_order(): Examples -------- >>> correct_order() - - Attributes - ---------- - attr : int - - Methods - ------- - method """ diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 255fadd91bd57..fb41b083c992e 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -2177,6 +2177,8 @@ enum NumpySectionOrder { ShortSummary, ExtendedSummary, Parameters, + Attributes, + Methods, Returns, Yields, Receives, @@ -2188,8 +2190,6 @@ enum NumpySectionOrder { Notes, References, Examples, - Attributes, - Methods, } fn numpy_section_order(kind: SectionKind) -> Option { @@ -2197,6 +2197,8 @@ fn numpy_section_order(kind: SectionKind) -> Option { SectionKind::ShortSummary => Some(NumpySectionOrder::ShortSummary), SectionKind::ExtendedSummary => Some(NumpySectionOrder::ExtendedSummary), SectionKind::Parameters => Some(NumpySectionOrder::Parameters), + SectionKind::Attributes => Some(NumpySectionOrder::Attributes), + SectionKind::Methods => Some(NumpySectionOrder::Methods), SectionKind::Returns => Some(NumpySectionOrder::Returns), SectionKind::Yields => Some(NumpySectionOrder::Yields), SectionKind::Receives => Some(NumpySectionOrder::Receives), @@ -2210,8 +2212,6 @@ fn numpy_section_order(kind: SectionKind) -> Option { SectionKind::Notes => Some(NumpySectionOrder::Notes), SectionKind::References => Some(NumpySectionOrder::References), SectionKind::Examples => Some(NumpySectionOrder::Examples), - SectionKind::Attributes => Some(NumpySectionOrder::Attributes), - SectionKind::Methods => Some(NumpySectionOrder::Methods), _ => None, } } From 7e477c21d5a5eaad0a2824e7a3ed121e0ae9fec8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 3 Mar 2026 11:18:30 -0500 Subject: [PATCH 182/261] [ty] Reduce the number of potentially-flaky projects (#23698) ## Summary Our results are much more stable now, but these three still appear as flaky (see, e.g., https://287ce299.ty-ecosystem-ext.pages.dev/diff). --- .../resources/primer/flaky.txt | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/crates/ty_python_semantic/resources/primer/flaky.txt b/crates/ty_python_semantic/resources/primer/flaky.txt index 66d21fd8683c0..48b344d87c9d8 100644 --- a/crates/ty_python_semantic/resources/primer/flaky.txt +++ b/crates/ty_python_semantic/resources/primer/flaky.txt @@ -1,31 +1,3 @@ -artigraph -bokeh -cloud-init -colour -core -dd-trace-py -Expression -ibis -jax -materialize -meson -openlibrary -pandas -pandas-stubs -pip -porcupine prefect pydantic -PyGithub -pylox -rich -rotki scikit-build-core -scikit-learn -scipy -setuptools -sockeye -spack -static-frame -sympy -vision From 747ce69e13247d4027daf0d98b8551668788b376 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 3 Mar 2026 16:24:06 +0000 Subject: [PATCH 183/261] [ty] Fix GitHub-annotations mdtest output format (#23694) --- crates/ty_test/src/lib.rs | 129 +++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 66 deletions(-) diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index dcc63b6508f9f..1b51f2590b386 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -98,25 +98,19 @@ pub fn run( EmbeddedFileSourceMap::new(&md_index, test_failures.backtick_offsets); for (relative_line_number, failures) in test_failures.by_line.iter() { - let file = match output_format { - OutputFormat::Cli => relative_fixture_path.as_str(), - OutputFormat::GitHub => absolute_fixture_path.as_str(), - }; + let file = relative_fixture_path.as_str(); let absolute_line_number = match source_map.to_absolute_line_number(relative_line_number) { Ok(line_number) => line_number, Err(last_line_number) => { - let _ = writeln!( - assertion, - "{}", - output_format.display_error( - file, - last_line_number, - "Found a trailing assertion comment \ + output_format.write_error( + &mut assertion, + file, + last_line_number, + "Found a trailing assertion comment \ (e.g., `# revealed:` or `# error:`) \ - not followed by any statement." - ) + not followed by any statement.", ); continue; @@ -124,10 +118,11 @@ pub fn run( }; for failure in failures { - let _ = writeln!( - assertion, - "{}", - output_format.display_error(file, absolute_line_number, failure) + output_format.write_error( + &mut assertion, + file, + absolute_line_number, + failure, ); } } @@ -136,18 +131,11 @@ pub fn run( if let Err(inconsistencies) = inconsistencies { any_failures = true; for inconsistency in inconsistencies { - match output_format { - OutputFormat::Cli => { - let info = relative_fixture_path.to_string().cyan(); - let _ = writeln!(assertion, " {info} {inconsistency}"); - } - OutputFormat::GitHub => { - let _ = writeln!( - assertion, - "::error file={absolute_fixture_path}::{inconsistency}" - ); - } - } + output_format.write_inconsistency( + &mut assertion, + relative_fixture_path, + &inconsistency, + ); } } @@ -191,46 +179,55 @@ impl OutputFormat { matches!(self, OutputFormat::Cli) } - fn display_error(self, file: &str, line: OneIndexed, failure: impl Display) -> impl Display { - struct Display<'a, T> { - format: OutputFormat, - file: &'a str, - line: OneIndexed, - failure: T, - } - - impl std::fmt::Display for Display<'_, T> - where - T: std::fmt::Display, - { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Display { - format, - file, - line, - failure, - } = self; - - match format { - OutputFormat::Cli => { - write!( - f, - " {file_line} {failure}", - file_line = format!("{file}:{line}").cyan() - ) - } - OutputFormat::GitHub => { - write!(f, "::error file={file},line={line}::{failure}") - } - } + /// Write a test error in the appropriate format. + /// + /// For CLI format, errors are appended to `assertion_buf` so they appear + /// in the assertion-failure message. + /// + /// For GitHub format, errors are printed directly to stdout so that GitHub + /// Actions can detect them as workflow commands. Workflow commands must + /// appear at the beginning of a line in stdout to be parsed by GitHub. + #[expect(clippy::print_stdout)] + fn write_error( + self, + assertion_buf: &mut String, + file: &str, + line: OneIndexed, + failure: impl Display, + ) { + match self { + OutputFormat::Cli => { + let _ = writeln!( + assertion_buf, + " {file_line} {failure}", + file_line = format!("{file}:{line}").cyan() + ); + } + OutputFormat::GitHub => { + println!("::error file={file},line={line}::{failure}"); } } + } - Display { - format: self, - file, - line, - failure, + /// Write a module-resolution inconsistency in the appropriate format. + /// + /// See [`write_error`](Self::write_error) for details on why GitHub-format + /// messages must be printed directly to stdout. + #[expect(clippy::print_stdout)] + fn write_inconsistency( + self, + assertion_buf: &mut String, + fixture_path: &Utf8Path, + inconsistency: &impl Display, + ) { + match self { + OutputFormat::Cli => { + let info = fixture_path.to_string().cyan(); + let _ = writeln!(assertion_buf, " {info} {inconsistency}"); + } + OutputFormat::GitHub => { + println!("::error file={fixture_path}::{inconsistency}"); + } } } } From c7db398395759a8d1b624b990726a013509864e3 Mon Sep 17 00:00:00 2001 From: Anish Giri <161533316+anishgirianish@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:45:03 -0600 Subject: [PATCH 184/261] [`perflint`] Extend `PERF102` to comprehensions and generators (#23473) ## Summary Extends PERF102 to catch .items() misuse in comprehensions and generators, not just for loops. Extracted the core detection into a shared helper (check_dict_items_usage) so both the for loop and comprehension paths can reuse it. The comprehension check runs between Step 2 and Step 3 in visit_expr since is_unused() needs the generator scope to still be active. now flagged _ = [k for k, _ in d.items()] # use .keys() _ = {v for _, v in d.items()} # use .values() _ = (v for _, v in d.items()) # use .values() still fine _ = [(k, v) for k, v in d.items()] # both used _ = [k for k, v in d.items() if v] # v used in condition ## Test Plan - Added error and no-error cases for list/set/dict comps, generators, and nested generators - Tests, clippy, and prek all pass Closes #6638 --------- Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../test/fixtures/perflint/PERF102.py | 15 + .../ast/analyze/deferred_comprehensions.rs | 37 ++ .../src/checkers/ast/analyze/mod.rs | 2 + .../ruff_linter/src/checkers/ast/deferred.rs | 1 + crates/ruff_linter/src/checkers/ast/mod.rs | 11 +- crates/ruff_linter/src/preview.rs | 7 + crates/ruff_linter/src/rules/perflint/mod.rs | 1 + .../perflint/rules/incorrect_dict_iterator.rs | 26 +- ...__perflint__tests__PERF102_PERF102.py.snap | 2 + ...t__tests__preview__PERF102_PERF102.py.snap | 376 ++++++++++++++++++ 10 files changed, 470 insertions(+), 8 deletions(-) create mode 100644 crates/ruff_linter/src/checkers/ast/analyze/deferred_comprehensions.rs create mode 100644 crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF102_PERF102.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF102.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF102.py index 137ee46693a22..b5991b457efc9 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF102.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF102.py @@ -105,3 +105,18 @@ def f(): def _create_context(name_to_value): for(B,D)in A.items(): if(C:=name_to_value.get(B.name)):A.run(B.set,C) + + +# Comprehensions and generators — errors (https://github.com/astral-sh/ruff/issues/6638) +_ = [k for k, _ in some_dict.items()] # PERF102 +_ = {k for k, _ in some_dict.items()} # PERF102 +_ = {k: "v" for k, _ in some_dict.items()} # PERF102 +_ = (k for k, _ in some_dict.items()) # PERF102 +_ = [v for _, v in some_dict.items()] # PERF102 +_ = [k for k, v in some_dict.items()] # PERF102 (v unused) +_ = [v for x in range(1) for _, v in some_dict.items()] # PERF102 + +# Comprehensions — no errors +_ = [(k, v) for k, v in some_dict.items()] # OK (both used) +_ = [item for item in some_dict.items()] # OK (not tuple target) +_ = [k for k, v in some_dict.items() if v] # OK (v used in condition) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_comprehensions.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_comprehensions.rs new file mode 100644 index 0000000000000..754692d9ace57 --- /dev/null +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_comprehensions.rs @@ -0,0 +1,37 @@ +use ruff_python_ast::Expr; + +use crate::checkers::ast::Checker; +use crate::codes::Rule; +use crate::rules::perflint; + +/// Run lint rules over all deferred comprehensions in the [`SemanticModel`]. +pub(crate) fn deferred_comprehensions(checker: &mut Checker) { + while !checker.analyze.comprehensions.is_empty() { + let comprehensions = std::mem::take(&mut checker.analyze.comprehensions); + for snapshot in comprehensions { + checker.semantic.restore(snapshot); + + let Some(generators) = + checker + .semantic + .current_expression() + .and_then(|expr| match expr { + Expr::ListComp(comp) => Some(comp.generators.as_slice()), + Expr::SetComp(comp) => Some(comp.generators.as_slice()), + Expr::DictComp(comp) => Some(comp.generators.as_slice()), + Expr::Generator(generator) => Some(generator.generators.as_slice()), + _ => None, + }) + else { + debug_assert!(false, "Expected a comprehension"); + continue; + }; + + for generator in generators { + if checker.is_rule_enabled(Rule::IncorrectDictIterator) { + perflint::rules::incorrect_dict_iterator_comprehension(checker, generator); + } + } + } + } +} diff --git a/crates/ruff_linter/src/checkers/ast/analyze/mod.rs b/crates/ruff_linter/src/checkers/ast/analyze/mod.rs index deeb55864b569..ce1d2f08aec76 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/mod.rs @@ -1,5 +1,6 @@ pub(super) use bindings::bindings; pub(super) use comprehension::comprehension; +pub(super) use deferred_comprehensions::deferred_comprehensions; pub(super) use deferred_for_loops::deferred_for_loops; pub(super) use deferred_lambdas::deferred_lambdas; pub(super) use deferred_scopes::deferred_scopes; @@ -16,6 +17,7 @@ pub(super) use unresolved_references::unresolved_references; mod bindings; mod comprehension; +mod deferred_comprehensions; mod deferred_for_loops; mod deferred_lambdas; mod deferred_scopes; diff --git a/crates/ruff_linter/src/checkers/ast/deferred.rs b/crates/ruff_linter/src/checkers/ast/deferred.rs index 01043e77d4505..aa4ec80094c25 100644 --- a/crates/ruff_linter/src/checkers/ast/deferred.rs +++ b/crates/ruff_linter/src/checkers/ast/deferred.rs @@ -34,4 +34,5 @@ pub(crate) struct Analyze { pub(crate) scopes: Vec, pub(crate) lambdas: Vec, pub(crate) for_loops: Vec, + pub(crate) comprehensions: Vec, } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index be0b789b6ed6b..cee5ce35bdc96 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -68,7 +68,9 @@ use crate::docstrings::extraction::ExtractionTarget; use crate::importer::{ImportRequest, Importer, ResolutionError}; use crate::noqa::NoqaMapping; use crate::package::PackageRoot; -use crate::preview::is_undefined_export_in_dunder_init_enabled; +use crate::preview::{ + is_incorrect_dict_iterator_comprehension_enabled, is_undefined_export_in_dunder_init_enabled, +}; use crate::registry::Rule; use crate::rules::flake8_bugbear::rules::ReturnInGenerator; use crate::rules::pyflakes::rules::{ @@ -2465,6 +2467,12 @@ impl<'a> Checker<'a> { for generator in generators { analyze::comprehension(generator, self); } + + if self.is_rule_enabled(Rule::IncorrectDictIterator) + && is_incorrect_dict_iterator_comprehension_enabled(self.settings()) + { + self.analyze.comprehensions.push(self.semantic.snapshot()); + } } /// Visit a body of [`Stmt`] nodes within a type-checking block. @@ -3309,6 +3317,7 @@ pub(crate) fn check_ast( // Check docstrings, bindings, and unresolved references. analyze::deferred_lambdas(&mut checker); analyze::deferred_for_loops(&mut checker); + analyze::deferred_comprehensions(&mut checker); analyze::definitions(&mut checker); analyze::bindings(&checker); analyze::unresolved_references(&checker); diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index d7531160c0baf..0a4afc6bae0e4 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -307,3 +307,10 @@ pub(crate) const fn is_expanded_import_conventions_enabled(preview: PreviewMode) pub(crate) const fn is_file_level_invalid_rule_code_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/23473 +pub(crate) const fn is_incorrect_dict_iterator_comprehension_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/perflint/mod.rs b/crates/ruff_linter/src/rules/perflint/mod.rs index 35a7d31f6278f..6f4ae7201fee2 100644 --- a/crates/ruff_linter/src/rules/perflint/mod.rs +++ b/crates/ruff_linter/src/rules/perflint/mod.rs @@ -31,6 +31,7 @@ mod tests { Ok(()) } + #[test_case(Rule::IncorrectDictIterator, Path::new("PERF102.py"))] // TODO: remove this test case when the fixes for `perf401` and `perf403` are stabilized #[test_case(Rule::ManualDictComprehension, Path::new("PERF403.py"))] #[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))] diff --git a/crates/ruff_linter/src/rules/perflint/rules/incorrect_dict_iterator.rs b/crates/ruff_linter/src/rules/perflint/rules/incorrect_dict_iterator.rs index b24e8fa79388b..3a1bbfeb61ff4 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/incorrect_dict_iterator.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/incorrect_dict_iterator.rs @@ -62,9 +62,21 @@ impl AlwaysFixableViolation for IncorrectDictIterator { } } -/// PERF102 +/// PERF102 for `for` loops. pub(crate) fn incorrect_dict_iterator(checker: &Checker, stmt_for: &ast::StmtFor) { - let Expr::Tuple(ast::ExprTuple { elts, .. }) = stmt_for.target.as_ref() else { + check_dict_items_usage(checker, stmt_for.target.as_ref(), stmt_for.iter.as_ref()); +} + +/// PERF102 for comprehensions and generators. +pub(crate) fn incorrect_dict_iterator_comprehension( + checker: &Checker, + comprehension: &ast::Comprehension, +) { + check_dict_items_usage(checker, &comprehension.target, &comprehension.iter); +} + +fn check_dict_items_usage(checker: &Checker, target: &Expr, iter: &Expr) { + let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else { return; }; let [key, value] = elts.as_slice() else { @@ -74,7 +86,7 @@ pub(crate) fn incorrect_dict_iterator(checker: &Checker, stmt_for: &ast::StmtFor func, arguments: Arguments { args, .. }, .. - }) = stmt_for.iter.as_ref() + }) = iter else { return; }; @@ -110,10 +122,10 @@ pub(crate) fn incorrect_dict_iterator(checker: &Checker, stmt_for: &ast::StmtFor let replace_target = Edit::range_replacement( pad( checker.locator().slice(value).to_string(), - stmt_for.target.range(), + target.range(), checker.locator(), ), - stmt_for.target.range(), + target.range(), ); diagnostic.set_fix(Fix::unsafe_edits(replace_attribute, [replace_target])); } @@ -129,10 +141,10 @@ pub(crate) fn incorrect_dict_iterator(checker: &Checker, stmt_for: &ast::StmtFor let replace_target = Edit::range_replacement( pad( checker.locator().slice(key).to_string(), - stmt_for.target.range(), + target.range(), checker.locator(), ), - stmt_for.target.range(), + target.range(), ); diagnostic.set_fix(Fix::unsafe_edits(replace_attribute, [replace_target])); } diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF102_PERF102.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF102_PERF102.py.snap index 587b67ccd09f4..913279a30773b 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF102_PERF102.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF102_PERF102.py.snap @@ -226,4 +226,6 @@ help: Replace `.items()` with `.keys()` - for(B,D)in A.items(): 106 + for B in A.keys(): 107 | if(C:=name_to_value.get(B.name)):A.run(B.set,C) +108 | +109 | note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF102_PERF102.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF102_PERF102.py.snap new file mode 100644 index 0000000000000..37227529245f2 --- /dev/null +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF102_PERF102.py.snap @@ -0,0 +1,376 @@ +--- +source: crates/ruff_linter/src/rules/perflint/mod.rs +--- +PERF102 [*] When using only the values of a dict use the `values()` method + --> PERF102.py:5:21 + | +4 | def f(): +5 | for _, value in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +6 | print(value) + | +help: Replace `.items()` with `.values()` +2 | +3 | +4 | def f(): + - for _, value in some_dict.items(): # PERF102 +5 + for value in some_dict.values(): # PERF102 +6 | print(value) +7 | +8 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:10:19 + | + 9 | def f(): +10 | for key, _ in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +11 | print(key) + | +help: Replace `.items()` with `.keys()` +7 | +8 | +9 | def f(): + - for key, _ in some_dict.items(): # PERF102 +10 + for key in some_dict.keys(): # PERF102 +11 | print(key) +12 | +13 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:15:30 + | +14 | def f(): +15 | for weird_arg_name, _ in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +16 | print(weird_arg_name) + | +help: Replace `.items()` with `.keys()` +12 | +13 | +14 | def f(): + - for weird_arg_name, _ in some_dict.items(): # PERF102 +15 + for weird_arg_name in some_dict.keys(): # PERF102 +16 | print(weird_arg_name) +17 | +18 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:20:25 + | +19 | def f(): +20 | for name, (_, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +21 | print(name) + | +help: Replace `.items()` with `.keys()` +17 | +18 | +19 | def f(): + - for name, (_, _) in some_dict.items(): # PERF102 +20 + for name in some_dict.keys(): # PERF102 +21 | print(name) +22 | +23 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:30:30 + | +29 | def f(): +30 | for (key1, _), (_, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +31 | print(key1) + | +help: Replace `.items()` with `.keys()` +27 | +28 | +29 | def f(): + - for (key1, _), (_, _) in some_dict.items(): # PERF102 +30 + for (key1, _) in some_dict.keys(): # PERF102 +31 | print(key1) +32 | +33 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the values of a dict use the `values()` method + --> PERF102.py:35:36 + | +34 | def f(): +35 | for (_, (_, _)), (value, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +36 | print(value) + | +help: Replace `.items()` with `.values()` +32 | +33 | +34 | def f(): + - for (_, (_, _)), (value, _) in some_dict.items(): # PERF102 +35 + for (value, _) in some_dict.values(): # PERF102 +36 | print(value) +37 | +38 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:50:32 + | +49 | def f(): +50 | for ((_, key2), (_, _)) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +51 | print(key2) + | +help: Replace `.items()` with `.keys()` +47 | +48 | +49 | def f(): + - for ((_, key2), (_, _)) in some_dict.items(): # PERF102 +50 + for (_, key2) in some_dict.keys(): # PERF102 +51 | print(key2) +52 | +53 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:85:25 + | +84 | def f(): +85 | for name, (_, _) in (some_function()).items(): # PERF102 + | ^^^^^^^^^^^^^^^^^^^^^^^ +86 | print(name) + | +help: Replace `.items()` with `.keys()` +82 | +83 | +84 | def f(): + - for name, (_, _) in (some_function()).items(): # PERF102 +85 + for name in (some_function()).keys(): # PERF102 +86 | print(name) +87 | +88 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:90:25 + | +89 | def f(): +90 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +91 | print(name) + | +help: Replace `.items()` with `.keys()` +87 | +88 | +89 | def f(): + - for name, (_, _) in (some_function().some_attribute).items(): # PERF102 +90 + for name in (some_function().some_attribute).keys(): # PERF102 +91 | print(name) +92 | +93 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:95:31 + | +94 | def f(): +95 | for name, unused_value in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +96 | print(name) + | +help: Replace `.items()` with `.keys()` +92 | +93 | +94 | def f(): + - for name, unused_value in some_dict.items(): # PERF102 +95 + for name in some_dict.keys(): # PERF102 +96 | print(name) +97 | +98 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the values of a dict use the `values()` method + --> PERF102.py:100:31 + | + 99 | def f(): +100 | for unused_name, value in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ +101 | print(value) + | +help: Replace `.items()` with `.values()` +97 | +98 | +99 | def f(): + - for unused_name, value in some_dict.items(): # PERF102 +100 + for value in some_dict.values(): # PERF102 +101 | print(value) +102 | +103 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:106:16 + | +104 | # Regression test for: https://github.com/astral-sh/ruff/issues/7097 +105 | def _create_context(name_to_value): +106 | for(B,D)in A.items(): + | ^^^^^^^ +107 | if(C:=name_to_value.get(B.name)):A.run(B.set,C) + | +help: Replace `.items()` with `.keys()` +103 | +104 | # Regression test for: https://github.com/astral-sh/ruff/issues/7097 +105 | def _create_context(name_to_value): + - for(B,D)in A.items(): +106 + for B in A.keys(): +107 | if(C:=name_to_value.get(B.name)):A.run(B.set,C) +108 | +109 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:111:20 + | +110 | # Comprehensions and generators — errors (https://github.com/astral-sh/ruff/issues/6638) +111 | _ = [k for k, _ in some_dict.items()] # PERF102 + | ^^^^^^^^^^^^^^^ +112 | _ = {k for k, _ in some_dict.items()} # PERF102 +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 + | +help: Replace `.items()` with `.keys()` +108 | +109 | +110 | # Comprehensions and generators — errors (https://github.com/astral-sh/ruff/issues/6638) + - _ = [k for k, _ in some_dict.items()] # PERF102 +111 + _ = [k for k in some_dict.keys()] # PERF102 +112 | _ = {k for k, _ in some_dict.items()} # PERF102 +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 +114 | _ = (k for k, _ in some_dict.items()) # PERF102 +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:112:20 + | +110 | # Comprehensions and generators — errors (https://github.com/astral-sh/ruff/issues/6638) +111 | _ = [k for k, _ in some_dict.items()] # PERF102 +112 | _ = {k for k, _ in some_dict.items()} # PERF102 + | ^^^^^^^^^^^^^^^ +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 +114 | _ = (k for k, _ in some_dict.items()) # PERF102 + | +help: Replace `.items()` with `.keys()` +109 | +110 | # Comprehensions and generators — errors (https://github.com/astral-sh/ruff/issues/6638) +111 | _ = [k for k, _ in some_dict.items()] # PERF102 + - _ = {k for k, _ in some_dict.items()} # PERF102 +112 + _ = {k for k in some_dict.keys()} # PERF102 +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 +114 | _ = (k for k, _ in some_dict.items()) # PERF102 +115 | _ = [v for _, v in some_dict.items()] # PERF102 +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:113:25 + | +111 | _ = [k for k, _ in some_dict.items()] # PERF102 +112 | _ = {k for k, _ in some_dict.items()} # PERF102 +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 + | ^^^^^^^^^^^^^^^ +114 | _ = (k for k, _ in some_dict.items()) # PERF102 +115 | _ = [v for _, v in some_dict.items()] # PERF102 + | +help: Replace `.items()` with `.keys()` +110 | # Comprehensions and generators — errors (https://github.com/astral-sh/ruff/issues/6638) +111 | _ = [k for k, _ in some_dict.items()] # PERF102 +112 | _ = {k for k, _ in some_dict.items()} # PERF102 + - _ = {k: "v" for k, _ in some_dict.items()} # PERF102 +113 + _ = {k: "v" for k in some_dict.keys()} # PERF102 +114 | _ = (k for k, _ in some_dict.items()) # PERF102 +115 | _ = [v for _, v in some_dict.items()] # PERF102 +116 | _ = [k for k, v in some_dict.items()] # PERF102 (v unused) +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:114:20 + | +112 | _ = {k for k, _ in some_dict.items()} # PERF102 +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 +114 | _ = (k for k, _ in some_dict.items()) # PERF102 + | ^^^^^^^^^^^^^^^ +115 | _ = [v for _, v in some_dict.items()] # PERF102 +116 | _ = [k for k, v in some_dict.items()] # PERF102 (v unused) + | +help: Replace `.items()` with `.keys()` +111 | _ = [k for k, _ in some_dict.items()] # PERF102 +112 | _ = {k for k, _ in some_dict.items()} # PERF102 +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 + - _ = (k for k, _ in some_dict.items()) # PERF102 +114 + _ = (k for k in some_dict.keys()) # PERF102 +115 | _ = [v for _, v in some_dict.items()] # PERF102 +116 | _ = [k for k, v in some_dict.items()] # PERF102 (v unused) +117 | _ = [v for x in range(1) for _, v in some_dict.items()] # PERF102 +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the values of a dict use the `values()` method + --> PERF102.py:115:20 + | +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 +114 | _ = (k for k, _ in some_dict.items()) # PERF102 +115 | _ = [v for _, v in some_dict.items()] # PERF102 + | ^^^^^^^^^^^^^^^ +116 | _ = [k for k, v in some_dict.items()] # PERF102 (v unused) +117 | _ = [v for x in range(1) for _, v in some_dict.items()] # PERF102 + | +help: Replace `.items()` with `.values()` +112 | _ = {k for k, _ in some_dict.items()} # PERF102 +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 +114 | _ = (k for k, _ in some_dict.items()) # PERF102 + - _ = [v for _, v in some_dict.items()] # PERF102 +115 + _ = [v for v in some_dict.values()] # PERF102 +116 | _ = [k for k, v in some_dict.items()] # PERF102 (v unused) +117 | _ = [v for x in range(1) for _, v in some_dict.items()] # PERF102 +118 | +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the keys of a dict use the `keys()` method + --> PERF102.py:116:20 + | +114 | _ = (k for k, _ in some_dict.items()) # PERF102 +115 | _ = [v for _, v in some_dict.items()] # PERF102 +116 | _ = [k for k, v in some_dict.items()] # PERF102 (v unused) + | ^^^^^^^^^^^^^^^ +117 | _ = [v for x in range(1) for _, v in some_dict.items()] # PERF102 + | +help: Replace `.items()` with `.keys()` +113 | _ = {k: "v" for k, _ in some_dict.items()} # PERF102 +114 | _ = (k for k, _ in some_dict.items()) # PERF102 +115 | _ = [v for _, v in some_dict.items()] # PERF102 + - _ = [k for k, v in some_dict.items()] # PERF102 (v unused) +116 + _ = [k for k in some_dict.keys()] # PERF102 (v unused) +117 | _ = [v for x in range(1) for _, v in some_dict.items()] # PERF102 +118 | +119 | # Comprehensions — no errors +note: This is an unsafe fix and may change runtime behavior + +PERF102 [*] When using only the values of a dict use the `values()` method + --> PERF102.py:117:38 + | +115 | _ = [v for _, v in some_dict.items()] # PERF102 +116 | _ = [k for k, v in some_dict.items()] # PERF102 (v unused) +117 | _ = [v for x in range(1) for _, v in some_dict.items()] # PERF102 + | ^^^^^^^^^^^^^^^ +118 | +119 | # Comprehensions — no errors + | +help: Replace `.items()` with `.values()` +114 | _ = (k for k, _ in some_dict.items()) # PERF102 +115 | _ = [v for _, v in some_dict.items()] # PERF102 +116 | _ = [k for k, v in some_dict.items()] # PERF102 (v unused) + - _ = [v for x in range(1) for _, v in some_dict.items()] # PERF102 +117 + _ = [v for x in range(1) for v in some_dict.values()] # PERF102 +118 | +119 | # Comprehensions — no errors +120 | _ = [(k, v) for k, v in some_dict.items()] # OK (both used) +note: This is an unsafe fix and may change runtime behavior From b017564060952f5a26fd68cb8a6c23577a13085b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 3 Mar 2026 12:58:54 -0500 Subject: [PATCH 185/261] [ty] Apply narrowing to walrus values (#23687) ## Summary Closes https://github.com/astral-sh/ty/issues/2947. See: https://github.com/astral-sh/ty/issues/626#issuecomment-3988412113. --- .../resources/mdtest/narrow/truthiness.md | 12 ++++++++++++ crates/ty_python_semantic/src/types/narrow.rs | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index 5d52cb47daae4..e6ab0527cb778 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -364,6 +364,18 @@ def f(): reveal_type(x) # revealed: (str & ~AlwaysTruthy) | None ``` +## Narrowing the value of a named expression + +The value expression on the right-hand side of the walrus operator should also be narrowed: + +```py +def foo(value: int | None): + if foo := value: + reveal_type(value) # revealed: int & ~AlwaysFalsy + else: + reveal_type(value) # revealed: (int & ~AlwaysTruthy) | None +``` + ## Narrowing a union of a `TypedDict` and `None` ```py diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index c5d67c13254c9..0163be2b1383e 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -831,7 +831,16 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { expr_named: &ast::ExprNamed, is_positive: bool, ) -> Option> { - self.evaluate_simple_expr(&expr_named.target, is_positive) + let target_constraints = self.evaluate_simple_expr(&expr_named.target, is_positive); + let value_constraints = self.evaluate_simple_expr(&expr_named.value, is_positive); + match (target_constraints, value_constraints) { + (Some(mut target), Some(value)) => { + merge_constraints_and(&mut target, value); + Some(target) + } + (Some(constraints), None) | (None, Some(constraints)) => Some(constraints), + (None, None) => None, + } } fn evaluate_expr_eq(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { @@ -2177,8 +2186,12 @@ impl<'db, 'a> PossiblyNarrowedPlacesBuilder<'db, 'a> { } // Boolean operations combine places from all sub-expressions ast::Expr::BoolOp(bool_op) => self.expr_bool_op(bool_op), - // Named expressions narrow the target - ast::Expr::Named(expr_named) => self.simple_expr(&expr_named.target), + // Named expressions narrow both the target and the value + ast::Expr::Named(expr_named) => { + let mut places = self.simple_expr(&expr_named.target); + places.extend(self.expression_node(&expr_named.value)); + places + } _ => PossiblyNarrowedPlaces::default(), } } From f8d1d29ec7f1ff0da7e0272c8388036f35e815b7 Mon Sep 17 00:00:00 2001 From: Dev-iL <6509619+Dev-iL@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:37:16 +0200 Subject: [PATCH 186/261] [`airflow`] Extract common utilities for use in new rules (#23630) Co-authored-by: Claude Opus 4.6 --- .../test/fixtures/airflow/AIR301_context.py | 34 +++++++++++++ .../ruff_linter/src/rules/airflow/helpers.rs | 36 ++++++++++++++ .../src/rules/airflow/rules/removal_in_3.rs | 23 +-------- ...flow__tests__AIR301_AIR301_context.py.snap | 49 +++++++++++++++++++ 4 files changed, 121 insertions(+), 21 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_context.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_context.py index 50af8d2b327a4..f84dd5a510be3 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_context.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_context.py @@ -205,3 +205,37 @@ def test_inlet_events_dataset_subscript_ok(**context): print(context["inlet_events"][Dataset("this://is-url")]) print(context["inlet_events"][Asset("this://is-url")]) + + +# Same context checks with airflow.sdk import +from airflow.sdk import task as sdk_task + + +@sdk_task +def sdk_access_deprecated_context_key(**context): + execution_date = context["execution_date"] + next_ds = context["next_ds"] + + +@sdk_task +def sdk_access_valid_context_key(**context): + logical_date = context["logical_date"] + + +# Test variant decorator forms like @task.branch and @task.short_circuit +@task.branch +def branch_task_with_deprecated_context(**context): + execution_date = context["execution_date"] + return "some_task" + + +@task.short_circuit +def short_circuit_task_with_deprecated_context(**context): + next_ds = context["next_ds"] + return True + + +@task.branch() +def branch_task_with_call_and_deprecated_context(**context): + tomorrow_ds = context["tomorrow_ds"] + return "some_task" diff --git a/crates/ruff_linter/src/rules/airflow/helpers.rs b/crates/ruff_linter/src/rules/airflow/helpers.rs index 0ba8067f08c9f..0a2263b30f078 100644 --- a/crates/ruff_linter/src/rules/airflow/helpers.rs +++ b/crates/ruff_linter/src/rules/airflow/helpers.rs @@ -3,6 +3,7 @@ use crate::fix::edits::remove_unused_imports; use crate::importer::ImportRequest; use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher}; use ruff_diagnostics::{Edit, Fix}; +use ruff_python_ast::helpers::map_callable; use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder}; use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::visitor::Visitor; @@ -290,3 +291,38 @@ where any_qualified_base_class(class_def, semantic, &is_base_class) } + +/// Returns `true` if the current statement hierarchy has a function that's decorated with +/// `@airflow.decorators.task` or `@airflow.sdk.task`. +pub(crate) fn in_airflow_task_function(semantic: &SemanticModel) -> bool { + semantic + .current_statements() + .find_map(|stmt| stmt.as_function_def_stmt()) + .is_some_and(|function_def| is_airflow_task(function_def, semantic)) +} + +/// Returns `true` if the given function is decorated with `@airflow.decorators.task` +/// (or `@airflow.sdk.task`), including variant forms like `@task.branch` and +/// `@task.short_circuit`. +pub(crate) fn is_airflow_task(function_def: &StmtFunctionDef, semantic: &SemanticModel) -> bool { + function_def.decorator_list.iter().any(|decorator| { + let expr = map_callable(&decorator.expression); + + // Match `@task` and `@task()` directly. + if semantic + .resolve_qualified_name(expr) + .is_some_and(|qn| matches!(qn.segments(), ["airflow", "decorators" | "sdk", "task"])) + { + return true; + } + + // Match `@task.` (e.g., `@task.branch`, `@task.short_circuit`). + if let Expr::Attribute(ExprAttribute { value, .. }) = expr { + return semantic.resolve_qualified_name(value).is_some_and(|qn| { + matches!(qn.segments(), ["airflow", "decorators" | "sdk", "task"]) + }); + } + + false + }) +} diff --git a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs index 56976c95b8e56..abef9e585662c 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs @@ -1,7 +1,8 @@ use crate::checkers::ast::Checker; use crate::rules::airflow::helpers::{ Replacement, generate_import_edit, generate_remove_and_runtime_import_edit, - is_airflow_builtin_or_provider, is_guarded_by_try_except, is_method_in_subclass, + in_airflow_task_function, is_airflow_builtin_or_provider, is_airflow_task, + is_guarded_by_try_except, is_method_in_subclass, }; use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_macros::{ViolationMetadata, derive_message_formats}; @@ -1223,26 +1224,6 @@ fn is_airflow_auth_manager(segments: &[&str]) -> bool { } } -/// Returns `true` if the current statement hierarchy has a function that's decorated with -/// `@airflow.decorators.task`. -fn in_airflow_task_function(semantic: &SemanticModel) -> bool { - semantic - .current_statements() - .find_map(|stmt| stmt.as_function_def_stmt()) - .is_some_and(|function_def| is_airflow_task(function_def, semantic)) -} - -/// Returns `true` if the given function is decorated with `@airflow.decorators.task`. -fn is_airflow_task(function_def: &StmtFunctionDef, semantic: &SemanticModel) -> bool { - function_def.decorator_list.iter().any(|decorator| { - semantic - .resolve_qualified_name(map_callable(&decorator.expression)) - .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["airflow", "decorators", "task"]) - }) - }) -} - /// Check it's "execute" method inherits from Airflow base operator /// /// For example: diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snap index 5f13ab65cd11d..e3d6a488f06ad 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snap @@ -521,3 +521,52 @@ AIR301 `inlet_events[""]` is removed in Airflow 3.0 | ^^^^^^^^^^^^^^^ | help: Accessing `inlet_events` via a string key is deprecated; use `context["inlet_events"][Asset(uri="this://is-url")]` instead of `context["inlet_events"]["this://is-url"]`. + +AIR301 `execution_date` is removed in Airflow 3.0 + --> AIR301_context.py:216:30 + | +214 | @sdk_task +215 | def sdk_access_deprecated_context_key(**context): +216 | execution_date = context["execution_date"] + | ^^^^^^^^^^^^^^^^ +217 | next_ds = context["next_ds"] + | + +AIR301 `next_ds` is removed in Airflow 3.0 + --> AIR301_context.py:217:23 + | +215 | def sdk_access_deprecated_context_key(**context): +216 | execution_date = context["execution_date"] +217 | next_ds = context["next_ds"] + | ^^^^^^^^^ + | + +AIR301 `execution_date` is removed in Airflow 3.0 + --> AIR301_context.py:228:30 + | +226 | @task.branch +227 | def branch_task_with_deprecated_context(**context): +228 | execution_date = context["execution_date"] + | ^^^^^^^^^^^^^^^^ +229 | return "some_task" + | + +AIR301 `next_ds` is removed in Airflow 3.0 + --> AIR301_context.py:234:23 + | +232 | @task.short_circuit +233 | def short_circuit_task_with_deprecated_context(**context): +234 | next_ds = context["next_ds"] + | ^^^^^^^^^ +235 | return True + | + +AIR301 `tomorrow_ds` is removed in Airflow 3.0 + --> AIR301_context.py:240:27 + | +238 | @task.branch() +239 | def branch_task_with_call_and_deprecated_context(**context): +240 | tomorrow_ds = context["tomorrow_ds"] + | ^^^^^^^^^^^^^ +241 | return "some_task" + | From 72742e70ea60bfc071329692d01f81511679234c Mon Sep 17 00:00:00 2001 From: Ismail Suddle Date: Tue, 3 Mar 2026 17:59:43 -0500 Subject: [PATCH 187/261] [ty] Improve folding for decorators (#23543) ## Summary This change removes decorators from the folding ranges of classes/functions. Resolves astral-sh/ty#2861 ## Test Plan Added snapshot tests to check the returned folding ranges. --- crates/ty_ide/src/folding_range.rs | 263 ++++++++++++++++++++++++++++- 1 file changed, 260 insertions(+), 3 deletions(-) diff --git a/crates/ty_ide/src/folding_range.rs b/crates/ty_ide/src/folding_range.rs index aeb7e9208f007..9da0fc4839587 100644 --- a/crates/ty_ide/src/folding_range.rs +++ b/crates/ty_ide/src/folding_range.rs @@ -1,8 +1,9 @@ use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; +use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_body}; -use ruff_python_ast::{AnyNodeRef, Stmt}; +use ruff_python_ast::{AnyNodeRef, Stmt, StmtClassDef, StmtFunctionDef}; use ruff_source_file::{Line, UniversalNewlines}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -51,6 +52,7 @@ pub fn folding_ranges(db: &dyn Db, file: File) -> Vec { let mut visitor = FoldingRangeVisitor { source: source.as_str(), ranges: vec![], + tokens: parsed.tokens(), }; visitor.visit_body(parsed.suite()); @@ -67,6 +69,7 @@ pub fn folding_ranges(db: &dyn Db, file: File) -> Vec { struct FoldingRangeVisitor<'a> { source: &'a str, ranges: Vec, + tokens: &'a Tokens, } impl<'a> FoldingRangeVisitor<'a> { @@ -223,6 +226,46 @@ impl<'a> FoldingRangeVisitor<'a> { } self.add_range(FoldingRange::from(first_stmt.range()).with_kind(FoldingRangeKind::Comment)); } + + /// Add a folding range for the function or class definition. + /// + /// `target` is checked for in `search_range`, and is used as the start if found. + fn add_def_range(&mut self, target: TokenKind, search_range: TextRange, end: TextSize) { + let target_token = self + .tokens + .in_range(search_range) + .iter() + .find(|tok| tok.kind() == target); + if let Some(tok) = target_token { + let range = TextRange::new(tok.start(), end); + self.add_range(range); + } + } + + /// Add a folding range for function definitions, excluding decorators. + fn add_function_def_range(&mut self, func: &StmtFunctionDef) { + if let Some(decorator) = func.decorator_list.last() { + let target = if func.is_async { + TokenKind::Async + } else { + TokenKind::Def + }; + let search_range = TextRange::new(decorator.end(), func.name.start()); + self.add_def_range(target, search_range, func.end()); + } else { + self.add_range(func.range()); + } + } + + /// Add a folding range for class definitions, excluding decorators. + fn add_class_def_range(&mut self, class: &StmtClassDef) { + if let Some(decorator) = class.decorator_list.last() { + let search_range = TextRange::new(decorator.end(), class.name.start()); + self.add_def_range(TokenKind::Class, search_range, class.end()); + } else { + self.add_range(class.range()); + } + } } impl SourceOrderVisitor<'_> for FoldingRangeVisitor<'_> { @@ -230,7 +273,7 @@ impl SourceOrderVisitor<'_> for FoldingRangeVisitor<'_> { match node { // Compound statements that create folding regions AnyNodeRef::StmtFunctionDef(func) => { - self.add_range(func.range()); + self.add_function_def_range(func); // Note that this may be duplicative with folding // ranges added for string literals. But I don't think // the LSP protocol specifies that this is a problem. @@ -241,7 +284,7 @@ impl SourceOrderVisitor<'_> for FoldingRangeVisitor<'_> { self.add_docstring_range(&func.body); } AnyNodeRef::StmtClassDef(class) => { - self.add_range(class.range()); + self.add_class_def_range(class); // See comment above for class docstrings about this // being duplicative with adding folding ranges for // string literals. @@ -1683,6 +1726,220 @@ with open("file.txt") as f: } } + #[test] + fn test_folding_range_decorated_function_single() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +@decorator +def my_function(): + pass + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @" + info[folding-range]: Folding Range + --> main.py:3:1 + | + 2 | @decorator + 3 | / def my_function(): + 4 | | pass + | |________^ + | + "); + } + + #[test] + fn test_folding_range_decorated_function_multiple() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +@first +@second +@third +def my_function(): + pass + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @" + info[folding-range]: Folding Range + --> main.py:5:1 + | + 3 | @second + 4 | @third + 5 | / def my_function(): + 6 | | pass + | |________^ + | + "); + } + + #[test] + fn test_folding_range_decorated_class_single() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +@dataclass +class MyClass: + value: int + name: str + +"#, + ) + .build(); + + // Single decorator is one line, so no decorator folding range is emitted. + assert_snapshot!(test.folding_ranges(), @" + info[folding-range]: Folding Range + --> main.py:3:1 + | + 2 | @dataclass + 3 | / class MyClass: + 4 | | value: int + 5 | | name: str + | |_____________^ + | + "); + } + + #[test] + fn test_folding_range_decorated_class_multiple() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +@decorator_a +@decorator_b +class MyClass: + value: int + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @" + info[folding-range]: Folding Range + --> main.py:4:1 + | + 2 | @decorator_a + 3 | @decorator_b + 4 | / class MyClass: + 5 | | value: int + | |______________^ + | + "); + } + + #[test] + fn test_folding_range_decorated_async_function() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +@decorator +async def my_async_function(): + pass + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @" + info[folding-range]: Folding Range + --> main.py:3:1 + | + 2 | @decorator + 3 | / async def my_async_function(): + 4 | | pass + | |________^ + | + "); + } + + #[test] + fn test_folding_range_decorated_nested_function() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +def outer_function(): + @decorator + def inner_function(): + pass + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / def outer_function(): + 3 | | @decorator + 4 | | def inner_function(): + 5 | | pass + | |____________^ + | + + info[folding-range]: Folding Range + --> main.py:4:5 + | + 2 | def outer_function(): + 3 | @decorator + 4 | / def inner_function(): + 5 | | pass + | |____________^ + | + "); + } + + #[test] + fn test_folding_range_decorated_async_method() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +class MyClass: + @decorator + async def my_async_method(self): + pass + +"#, + ) + .build(); + + assert_snapshot!(test.folding_ranges(), @" + info[folding-range]: Folding Range + --> main.py:2:1 + | + 2 | / class MyClass: + 3 | | @decorator + 4 | | async def my_async_method(self): + 5 | | pass + | |____________^ + | + + info[folding-range]: Folding Range + --> main.py:4:5 + | + 2 | class MyClass: + 3 | @decorator + 4 | / async def my_async_method(self): + 5 | | pass + | |____________^ + | + "); + } + struct FoldingRangeDiagnostic { file: File, folding_range: FoldingRange, From 429aa7b52d7ba441e7d03f3a4a90c9e388c3fff9 Mon Sep 17 00:00:00 2001 From: Peter Rizzi <32918283+rizzip@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:49:53 -0500 Subject: [PATCH 188/261] [ty] Fix type checking for multi-member enums within in a function block (#23683) ## Summary Infer precise union types for `.name` and `.value` on an enum type, instead of `Any`. Minimal example: ```py from enum import Enum class MyEnum(Enum): up = 0 down = 1 def f(x: MyEnum): # These were both previously `Any`: reveal_type(x.name) # revealed: Literal["up", "down"] reveal_type(x.value) # revealed: Literal[0, 1] ``` ## Test Plan Added mdtest. --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/enums.md | 56 ++++++++++++++++--- crates/ty_python_semantic/src/types.rs | 53 +++++++++--------- crates/ty_python_semantic/src/types/enums.rs | 52 +++++++++++++++++ 3 files changed, 128 insertions(+), 33 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 5455c3dff4e60..112cf380dea7a 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -990,9 +990,8 @@ reveal_type(Color.RED._name_) # revealed: Literal["RED"] def _(red_or_blue: Literal[Color.RED, Color.BLUE]): reveal_type(red_or_blue.name) # revealed: Literal["RED", "BLUE"] -def _(any_color: Color): - # TODO: Literal["RED", "GREEN", "BLUE"] - reveal_type(any_color.name) # revealed: Any +def _(color: Color): + reveal_type(color.name) # revealed: Literal["RED", "GREEN", "BLUE"] ``` ### `value` and `_value_` @@ -1017,6 +1016,9 @@ reveal_type(Color.RED._value_) # revealed: Literal[1] reveal_type(Color.GREEN.value) # revealed: Literal[2] reveal_type(Color.GREEN._value_) # revealed: Literal[2] +def _(color: Color): + reveal_type(color.value) # revealed: Literal[1, 2, 3] + class Answer(StrEnum): YES = "yes" NO = "no" @@ -1026,6 +1028,9 @@ reveal_type(Answer.YES._value_) # revealed: Literal["yes"] reveal_type(Answer.NO.value) # revealed: Literal["no"] reveal_type(Answer.NO._value_) # revealed: Literal["no"] + +def _(answer: Answer): + reveal_type(answer.value) # revealed: Literal["yes", "no"] ``` ## Properties of enum types @@ -1140,6 +1145,9 @@ python-version = "3.9" from enum import Enum, EnumMeta class EnumWithEnumMetaMetaclass(metaclass=EnumMeta): + # Using `EnumMeta` as a metaclass without inheriting `Enum` requires an `__init__` + # method that will accept member values (TODO we could catch the lack of this): + def __init__(self, val): ... NO = 0 YES = 1 @@ -1148,24 +1156,37 @@ reveal_type(EnumWithEnumMetaMetaclass.NO) # revealed: Literal[EnumWithEnumMetaM class SubclassOfEnumMeta(EnumMeta): ... class EnumWithSubclassOfEnumMetaMetaclass(metaclass=SubclassOfEnumMeta): + def __init__(self, val): ... NO = 0 YES = 1 reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO) # revealed: Literal[EnumWithSubclassOfEnumMetaMetaclass.NO] -# Attributes like `.value` can *not* be accessed on members of these enums: +# Attributes `.value` and `.name` can *not* be accessed on members of these enums: + # error: [unresolved-attribute] EnumWithSubclassOfEnumMetaMetaclass.NO.value # error: [unresolved-attribute] -EnumWithSubclassOfEnumMetaMetaclass.NO._value_ -# error: [unresolved-attribute] EnumWithSubclassOfEnumMetaMetaclass.NO.name -# error: [unresolved-attribute] -EnumWithSubclassOfEnumMetaMetaclass.NO._name_ + +# But the internal underscore attributes are available: + +reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO._value_) # revealed: Any +reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO._name_) # revealed: Literal["NO"] + +def _(x: EnumWithSubclassOfEnumMetaMetaclass): + # error: [unresolved-attribute] + x.value + # error: [unresolved-attribute] + x.name + reveal_type(x._value_) # revealed: Any + reveal_type(x._name_) # revealed: Literal["NO", "YES"] ``` ### Enums with (subclasses of) `EnumType` as metaclass +In Python 3.11, the meta-type was renamed to `EnumType`. + ```toml [environment] python-version = "3.11" @@ -1175,6 +1196,7 @@ python-version = "3.11" from enum import Enum, EnumType class EnumWithEnumMetaMetaclass(metaclass=EnumType): + def __init__(self, val): ... NO = 0 YES = 1 @@ -1183,13 +1205,31 @@ reveal_type(EnumWithEnumMetaMetaclass.NO) # revealed: Literal[EnumWithEnumMetaM class SubclassOfEnumMeta(EnumType): ... class EnumWithSubclassOfEnumMetaMetaclass(metaclass=SubclassOfEnumMeta): + def __init__(self, val): ... NO = 0 YES = 1 reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO) # revealed: Literal[EnumWithSubclassOfEnumMetaMetaclass.NO] +# Attributes `.value` and `.name` can *not* be accessed on members of these enums: + # error: [unresolved-attribute] EnumWithSubclassOfEnumMetaMetaclass.NO.value +# error: [unresolved-attribute] +EnumWithSubclassOfEnumMetaMetaclass.NO.name + +# But the internal underscore attributes are available: + +reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO._value_) # revealed: Any +reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO._name_) # revealed: Literal["NO"] + +def _(x: EnumWithSubclassOfEnumMetaMetaclass): + # error: [unresolved-attribute] + x.value + # error: [unresolved-attribute] + x.name + reveal_type(x._value_) # revealed: Any + reveal_type(x._name_) # revealed: Literal["NO", "YES"] ``` ## Function syntax diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f5c5aae538294..088b2ee267516 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -58,7 +58,7 @@ use crate::types::constraints::{ use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM}; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; -use crate::types::enums::{enum_metadata, is_single_member_enum}; +use crate::types::enums::enum_metadata; use crate::types::function::{ DataclassTransformerFlags, DataclassTransformerParams, FunctionDecorators, FunctionSpans, FunctionType, KnownFunction, @@ -3308,26 +3308,22 @@ impl<'db> Type<'db> { .member_lookup_with_policy(db, name, policy), Type::LiteralValue(literal) - if literal.as_enum().is_some_and(|enum_literal| { - matches!(name_str, "name" | "_name_") - && Type::ClassLiteral(enum_literal.enum_class(db)) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) - }) => + if literal.as_enum().is_some() + && matches!(name_str, "name" | "_name_" | "value" | "_value_") => { let enum_literal = literal.as_enum().unwrap(); - Place::bound(Type::string_literal(db, enum_literal.name(db))).into() - } - - Type::LiteralValue(literal) - if literal.as_enum().is_some_and(|enum_literal| { - matches!(name_str, "value" | "_value_") - && Type::ClassLiteral(enum_literal.enum_class(db)) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) - }) => - { - let enum_literal = literal.as_enum().unwrap(); - enum_metadata(db, enum_literal.enum_class(db)) - .and_then(|metadata| metadata.value_type(enum_literal.name(db))) + let enum_class = enum_literal.enum_class(db); + let is_enum_subclass = Type::ClassLiteral(enum_class) + .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); + + enum_metadata(db, enum_class) + .and_then(|metadata| match name_str { + "name" if is_enum_subclass => metadata.name_type(db, enum_literal.name(db)), + "_name_" => metadata.name_type(db, enum_literal.name(db)), + "value" if is_enum_subclass => metadata.value_type(enum_literal.name(db)), + "_value_" => metadata.value_type(enum_literal.name(db)), + _ => None, + }) .map_or_else(|| Place::Undefined, Place::bound) .into() } @@ -3346,13 +3342,20 @@ impl<'db> Type<'db> { } Type::NominalInstance(instance) - if matches!(name_str, "value" | "_value_") - && is_single_member_enum(db, instance.class_literal(db)) => + if matches!(name_str, "name" | "_name_" | "value" | "_value_") + && enum_metadata(db, instance.class_literal(db)).is_some() => { - enum_metadata(db, instance.class_literal(db)) - .and_then(|metadata| { - let (name, _) = metadata.members.get_index(0)?; - metadata.value_type(name) + let class_literal = instance.class_literal(db); + let is_enum_subclass = Type::ClassLiteral(class_literal) + .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); + + enum_metadata(db, class_literal) + .and_then(|metadata| match name_str { + "name" if is_enum_subclass => metadata.instance_name_type(db), + "_name_" => metadata.instance_name_type(db), + "value" if is_enum_subclass => metadata.instance_value_type(db), + "_value_" => metadata.instance_value_type(db), + _ => None, }) .map_or_else(Place::default, Place::bound) .into() diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 0f724641a7b5b..ab4461f5d46c6 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -11,6 +11,7 @@ use crate::{ types::{ ClassBase, ClassLiteral, DynamicType, EnumLiteralType, KnownClass, LiteralValueTypeKind, MemberLookupPolicy, StaticClassLiteral, Type, TypeQualifiers, function::FunctionType, + set_theoretic::builder::UnionBuilder, }, }; @@ -62,6 +63,57 @@ impl<'db> EnumMetadata<'db> { } } + /// Returns the type of `.name`/`._name_` for a given enum member. + /// + /// This is always a string literal of the member name. + pub(crate) fn name_type(&self, db: &'db dyn Db, member_name: &Name) -> Option> { + self.members + .contains_key(member_name) + .then(|| Type::string_literal(db, member_name.as_str())) + } + + /// Returns the type of `.value`/`._value_` for an enum instance that is not + /// narrowed to a specific member (e.g. `x: MyEnum` where `MyEnum` has multiple members). + /// + /// If there is an explicit `_value_` annotation, returns that. + /// If there is a custom `__init__`, returns `Any`. + /// Otherwise, returns the union of all member value types. + pub(crate) fn instance_value_type(&self, db: &'db dyn Db) -> Option> { + if self.members.is_empty() { + return None; + } + if let Some(annotation) = self.value_annotation { + Some(annotation) + } else if self.init_function.is_some() { + Some(Type::Dynamic(DynamicType::Any)) + } else { + let union = self + .members + .values() + .copied() + .fold(UnionBuilder::new(db), UnionBuilder::add) + .build(); + Some(union) + } + } + + /// Returns the type of `.name`/`._name_` for an enum instance that is not + /// narrowed to a specific member (e.g. `x: MyEnum` where `MyEnum` has multiple members). + /// + /// Returns the union of all member name string literals. + pub(crate) fn instance_name_type(&self, db: &'db dyn Db) -> Option> { + if self.members.is_empty() { + return None; + } + let union = self + .members + .keys() + .map(|name| Type::string_literal(db, name.as_str())) + .fold(UnionBuilder::new(db), UnionBuilder::add) + .build(); + Some(union) + } + pub(crate) fn resolve_member<'a>(&'a self, name: &'a Name) -> Option<&'a Name> { if self.members.contains_key(name) { Some(name) From d740e84eefb40c614be537fe22040cb9f7e257bc Mon Sep 17 00:00:00 2001 From: chiri Date: Wed, 4 Mar 2026 04:27:47 +0300 Subject: [PATCH 189/261] [`refurb`] Fix `FURB101` and `FURB103` false positives when I/O variable is used later (#23542) Co-authored-by: Amethyst Reese --- .../test/fixtures/refurb/FURB101_2.py | 21 +++++ .../test/fixtures/refurb/FURB103_2.py | 26 +++++++ .../ast/analyze/deferred_with_statements.rs | 23 ++++++ .../src/checkers/ast/analyze/mod.rs | 2 + .../src/checkers/ast/analyze/statement.rs | 10 +-- .../ruff_linter/src/checkers/ast/deferred.rs | 1 + crates/ruff_linter/src/checkers/ast/mod.rs | 1 + .../ruff_linter/src/rules/refurb/helpers.rs | 34 +++++--- crates/ruff_linter/src/rules/refurb/mod.rs | 2 + ...__refurb__tests__FURB101_FURB101_2.py.snap | 47 +++++++++++ ...__refurb__tests__FURB103_FURB103_2.py.snap | 78 +++++++++++++++++++ 11 files changed, 228 insertions(+), 17 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/refurb/FURB101_2.py create mode 100644 crates/ruff_linter/resources/test/fixtures/refurb/FURB103_2.py create mode 100644 crates/ruff_linter/src/checkers/ast/analyze/deferred_with_statements.rs create mode 100644 crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_2.py.snap create mode 100644 crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_2.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_2.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_2.py new file mode 100644 index 0000000000000..11b66cf125544 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_2.py @@ -0,0 +1,21 @@ +# FURB101 +with open("file.txt", encoding="utf-8") as f: + _ = f.read() +f = object() +print(f) + +# See: https://github.com/astral-sh/ruff/issues/21483 +with open("file.txt", encoding="utf-8") as f: + _ = f.read() +print(f.mode) + +# Rebinding in a later `with ... as config_file` should not suppress this one. +with open("config.yaml", encoding="utf-8") as config_file: + config_raw = config_file.read() + +if "tts:" in config_raw: + try: + with open("config.yaml", "w", encoding="utf-8") as config_file: + config_file.write(config_raw.replace("tts:", "google_translate:")) + except OSError: + pass diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_2.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_2.py new file mode 100644 index 0000000000000..0de353e0bdf69 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_2.py @@ -0,0 +1,26 @@ +# FURB103 +# should trigger +with open("file.txt", "w", encoding="utf-8") as f: + f.write("\n") +f = object() +print(f) + +# See: https://github.com/astral-sh/ruff/issues/21483 +with open("file.txt", "w") as f: + f.write("\n") +print(f.encoding) + + +def _(): + # should trigger + with open("file.txt", "w") as f: + f.write("\n") + return (f.name for _ in [0]) + + +def _set(): + # should trigger + with open("file.txt", "w") as f: + f.write("\n") + g = {f.name for _ in [0]} + return g diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_with_statements.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_with_statements.rs new file mode 100644 index 0000000000000..db2afcafc6a70 --- /dev/null +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_with_statements.rs @@ -0,0 +1,23 @@ +use ruff_python_ast::Stmt; + +use crate::{checkers::ast::Checker, codes::Rule, rules::refurb}; + +/// Run lint rules over all deferred with-statements in the [`SemanticModel`]. +pub(crate) fn deferred_with_statements(checker: &mut Checker) { + while !checker.analyze.with_statements.is_empty() { + let with_statements = std::mem::take(&mut checker.analyze.with_statements); + for snapshot in with_statements { + checker.semantic.restore(snapshot); + + let Stmt::With(stmt_with) = checker.semantic.current_statement() else { + unreachable!("Expected Stmt::With"); + }; + if checker.is_rule_enabled(Rule::ReadWholeFile) { + refurb::rules::read_whole_file(checker, stmt_with); + } + if checker.is_rule_enabled(Rule::WriteWholeFile) { + refurb::rules::write_whole_file(checker, stmt_with); + } + } + } +} diff --git a/crates/ruff_linter/src/checkers/ast/analyze/mod.rs b/crates/ruff_linter/src/checkers/ast/analyze/mod.rs index ce1d2f08aec76..b3c149690fd5a 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/mod.rs @@ -4,6 +4,7 @@ pub(super) use deferred_comprehensions::deferred_comprehensions; pub(super) use deferred_for_loops::deferred_for_loops; pub(super) use deferred_lambdas::deferred_lambdas; pub(super) use deferred_scopes::deferred_scopes; +pub(super) use deferred_with_statements::deferred_with_statements; pub(super) use definitions::definitions; pub(super) use except_handler::except_handler; pub(super) use expression::expression; @@ -21,6 +22,7 @@ mod deferred_comprehensions; mod deferred_for_loops; mod deferred_lambdas; mod deferred_scopes; +mod deferred_with_statements; mod definitions; mod except_handler; mod expression; diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 1fa1042233451..51f2fa74f5383 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1181,11 +1181,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.is_rule_enabled(Rule::RedefinedLoopName) { pylint::rules::redefined_loop_name(checker, stmt); } - if checker.is_rule_enabled(Rule::ReadWholeFile) { - refurb::rules::read_whole_file(checker, with_stmt); - } - if checker.is_rule_enabled(Rule::WriteWholeFile) { - refurb::rules::write_whole_file(checker, with_stmt); + if checker.any_rule_enabled(&[Rule::ReadWholeFile, Rule::WriteWholeFile]) { + checker + .analyze + .with_statements + .push(checker.semantic.snapshot()); } if checker.is_rule_enabled(Rule::UselessWithLock) { pylint::rules::useless_with_lock(checker, with_stmt); diff --git a/crates/ruff_linter/src/checkers/ast/deferred.rs b/crates/ruff_linter/src/checkers/ast/deferred.rs index aa4ec80094c25..5dc9bfe789114 100644 --- a/crates/ruff_linter/src/checkers/ast/deferred.rs +++ b/crates/ruff_linter/src/checkers/ast/deferred.rs @@ -34,5 +34,6 @@ pub(crate) struct Analyze { pub(crate) scopes: Vec, pub(crate) lambdas: Vec, pub(crate) for_loops: Vec, + pub(crate) with_statements: Vec, pub(crate) comprehensions: Vec, } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index cee5ce35bdc96..475839030296a 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -3321,6 +3321,7 @@ pub(crate) fn check_ast( analyze::definitions(&mut checker); analyze::bindings(&checker); analyze::unresolved_references(&checker); + analyze::deferred_with_statements(&mut checker); // Reset the scope to module-level, and check all consumed scopes. checker.semantic.scope_id = ScopeId::global(); diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index 3b1d20e7844ce..70666e43b49b0 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -217,23 +217,29 @@ fn resolve_file_open<'a>( if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() { return None; } + let var = item.optional_vars.as_deref()?.as_name_expr()?; let scope = semantic.current_scope(); - let binding = scope.get_all(var.id.as_str()).find_map(|id| { - let b = semantic.binding(id); - (b.range() == var.range()).then_some(b) + let binding = semantic.binding(id); + (binding.range() == var.range()).then_some(binding) })?; - let references: Vec<&ResolvedReference> = binding - .references - .iter() - .map(|id| semantic.reference(*id)) - .filter(|reference| with.range().contains_range(reference.range())) - .collect(); - - let [reference] = references.as_slice() else { + let mut binding_references = binding + .references() + .map(|id| semantic.reference(id)) + // Reassignments in the same scope can carry forward older references. Ignore anything + // that appears before this `with` statement and only consider references from this point. + .filter(|reference| { + reference.scope_id() == binding.scope && reference.start() >= with.start() + }); + + let reference = binding_references.next()?; + if binding_references.next().is_some() { return None; - }; + } + if !with.range().contains_range(reference.range()) { + return None; + } Some(FileOpen { item, @@ -279,6 +285,7 @@ fn find_file_open<'a>( let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?; let mode = kw_mode.unwrap_or(pos_mode); + resolve_file_open( item, with, @@ -307,9 +314,11 @@ fn find_path_open<'a>( { return None; } + if !is_open_call_from_pathlib(func, semantic) { return None; } + let attr = func.as_attribute_expr()?; let mode = if args.is_empty() { OpenMode::ReadText @@ -319,6 +328,7 @@ fn find_path_open<'a>( let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?; let mode = kw_mode.unwrap_or(mode); + resolve_file_open( item, with, diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index a98a770c93ce2..2e32970a4028b 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -17,6 +17,7 @@ mod tests { #[test_case(Rule::ReadWholeFile, Path::new("FURB101_0.py"))] #[test_case(Rule::ReadWholeFile, Path::new("FURB101_1.py"))] + #[test_case(Rule::ReadWholeFile, Path::new("FURB101_2.py"))] #[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))] #[test_case(Rule::IfExpInsteadOfOrOperator, Path::new("FURB110.py"))] #[test_case(Rule::ReimplementedOperator, Path::new("FURB118.py"))] @@ -49,6 +50,7 @@ mod tests { #[test_case(Rule::ListReverseCopy, Path::new("FURB187.py"))] #[test_case(Rule::WriteWholeFile, Path::new("FURB103_0.py"))] #[test_case(Rule::WriteWholeFile, Path::new("FURB103_1.py"))] + #[test_case(Rule::WriteWholeFile, Path::new("FURB103_2.py"))] #[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))] #[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))] #[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))] diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_2.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_2.py.snap new file mode 100644 index 0000000000000..00d2098e90620 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_2.py.snap @@ -0,0 +1,47 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +assertion_line: 65 +--- +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")` + --> FURB101_2.py:2:6 + | +1 | # FURB101 +2 | with open("file.txt", encoding="utf-8") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +3 | _ = f.read() +4 | f = object() + | +help: Replace with `Path("file.txt").read_text(encoding="utf-8")` +1 | # FURB101 + - with open("file.txt", encoding="utf-8") as f: + - _ = f.read() +2 + import pathlib +3 + _ = pathlib.Path("file.txt").read_text(encoding="utf-8") +4 | f = object() +5 | print(f) +6 | + +FURB101 [*] `open` and `read` should be replaced by `Path("config.yaml").read_text(encoding="utf-8")` + --> FURB101_2.py:13:6 + | +12 | # Rebinding in a later `with ... as config_file` should not suppress this one. +13 | with open("config.yaml", encoding="utf-8") as config_file: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +14 | config_raw = config_file.read() + | +help: Replace with `Path("config.yaml").read_text(encoding="utf-8")` +1 | # FURB101 +2 + import pathlib +3 | with open("file.txt", encoding="utf-8") as f: +4 | _ = f.read() +5 | f = object() +-------------------------------------------------------------------------------- +11 | print(f.mode) +12 | +13 | # Rebinding in a later `with ... as config_file` should not suppress this one. + - with open("config.yaml", encoding="utf-8") as config_file: + - config_raw = config_file.read() +14 + config_raw = pathlib.Path("config.yaml").read_text(encoding="utf-8") +15 | +16 | if "tts:" in config_raw: +17 | try: diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_2.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_2.py.snap new file mode 100644 index 0000000000000..8564dd17b0789 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_2.py.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("\n", encoding="utf-8")` + --> FURB103_2.py:3:6 + | +1 | # FURB103 +2 | # should trigger +3 | with open("file.txt", "w", encoding="utf-8") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4 | f.write("\n") +5 | f = object() + | +help: Replace with `Path("file.txt").write_text("\n", encoding="utf-8")` +1 | # FURB103 +2 | # should trigger + - with open("file.txt", "w", encoding="utf-8") as f: + - f.write("\n") +3 + import pathlib +4 + pathlib.Path("file.txt").write_text("\n", encoding="utf-8") +5 | f = object() +6 | print(f) +7 | + +FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("\n")` + --> FURB103_2.py:16:10 + | +14 | def _(): +15 | # should trigger +16 | with open("file.txt", "w") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | f.write("\n") +18 | return (f.name for _ in [0]) + | +help: Replace with `Path("file.txt").write_text("\n")` +1 | # FURB103 +2 | # should trigger +3 + import pathlib +4 | with open("file.txt", "w", encoding="utf-8") as f: +5 | f.write("\n") +6 | f = object() +-------------------------------------------------------------------------------- +14 | +15 | def _(): +16 | # should trigger + - with open("file.txt", "w") as f: + - f.write("\n") +17 + pathlib.Path("file.txt").write_text("\n") +18 | return (f.name for _ in [0]) +19 | +20 | + +FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("\n")` + --> FURB103_2.py:23:10 + | +21 | def _set(): +22 | # should trigger +23 | with open("file.txt", "w") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +24 | f.write("\n") +25 | g = {f.name for _ in [0]} + | +help: Replace with `Path("file.txt").write_text("\n")` +1 | # FURB103 +2 | # should trigger +3 + import pathlib +4 | with open("file.txt", "w", encoding="utf-8") as f: +5 | f.write("\n") +6 | f = object() +-------------------------------------------------------------------------------- +21 | +22 | def _set(): +23 | # should trigger + - with open("file.txt", "w") as f: + - f.write("\n") +24 + pathlib.Path("file.txt").write_text("\n") +25 | g = {f.name for _ in [0]} +26 | return g From 7eb47ac80e2250af06ebb8ee1109a7115803117b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 3 Mar 2026 21:06:00 -0500 Subject: [PATCH 190/261] [ty] Fix union `*args` binding for optional positional parameters (#23124) ## Summary Closes https://github.com/astral-sh/ty/issues/2734. --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/call/function.md | 84 +++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 119 +++++++++++++++--- 2 files changed, 189 insertions(+), 14 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index c4325a9fe2da5..6d2d367c8e436 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -786,6 +786,53 @@ for tup in my_other_args: f4(*tup, e=None) ``` +Regression test for . + +```py +def f5(x: int | None = None, y: str = "") -> None: ... +def f6(flag: bool) -> None: + args = () if flag else (1,) + f5(*args) + +def f7(x: int | None = None, y: str = "") -> None: ... +def f8(flag: bool) -> None: + args = () if flag else ("bad",) + f7(*args) # error: [invalid-argument-type] + +def f11(*args: int) -> None: ... +def f12(args: tuple[int] | int) -> None: + f11(*args) # error: [not-iterable] + +def f13(a: int, b: int, c: str) -> None: ... +def f14(a: int, b: int, c: str, d: list[float], e: list[float]) -> None: ... +def f15(profile: bool, line: str) -> None: + matcher = f13 + timings = [] + if profile: + matcher = f14 + timings = [[0.0], [1.0], [2.0], [3.0]] + matcher(1, 2, line, *timings[:2]) + +def f9(x: int = 0, y: str = "") -> None: ... +def f10(args: tuple[int, ...] | tuple[int, str]) -> None: + # The variable-length element `int` from `tuple[int, ...]` unions with `str` + # from `tuple[int, str]` at position 1, giving `int | str` for `y: str`. + f9(*args) # error: [invalid-argument-type] + +def f18(x: int = 0, y: int = 0) -> None: ... +def f19(args: tuple[int, ...] | tuple[int, int]) -> None: + f18(*args) + +# TODO: Union variadic unpacking should also work when the non-defaulted parameters +# are covered by all union elements, even if not all remaining parameters are defaulted. +# Currently we only apply per-element iteration when all remaining positional parameters +# have defaults, so this falls back to `iterate()` which produces `tuple[int, ...]` and +# greedily matches `c: str` with `int`. +def f16(a: int, b: int = 0, c: str = "") -> None: ... +def f17(x: tuple[int] | tuple[int, int]) -> None: + f16(*x) # error: [invalid-argument-type] # TODO: false positive +``` + ### Mixed argument and parameter containing variadic ```toml @@ -1512,3 +1559,40 @@ def _(arg: int): # error: [not-iterable] "Object of type `int` is not iterable" foo(*arg) ``` + +## Union variadic unpacking with explicit keyword arguments + +When a union type containing variable-length elements (like `Unknown`) is unpacked as `*args`, the +variadic expansion should not greedily consume optional positional parameters that are also provided +as explicit keyword arguments. + +```py +from ty_extensions import Unknown + +def f(a: int = 0, b: int = 0, c: int = 0, fmt: str | None = None) -> None: ... +def _(args: "Unknown | tuple[int, int, int]"): + f(*args, fmt="{key}") # fine +``` + +## Variadic unpacking should stop at max known arity + +When unpacking (a union of) fixed-length tuples, variadic matching should stop once the known +positions are exhausted. Otherwise, optional positional parameters can be incorrectly treated as +already assigned, causing false positives for `**kwargs`. + +(This test uses `**kwargs` unpacking of a `TypedDict` instead of the simpler `c=1` keyword argument, +because `c=1` is a known keyword argument and we always prevent unpacking `*args` over an +explicitly-provided keyword argument. The case shown here, without the explicit keyword argument, +requires instead that we use our knowledge of the tuple length to prevent over-unpacking.) + +```py +from typing import TypedDict + +class CKwargs(TypedDict): + c: int + +def f(a: int = 0, b: int = 0, c: int = 0) -> None: ... +def _(args_tuple: tuple[int, int], args_union: tuple[int] | tuple[int, int], kwargs: CKwargs) -> None: + f(*args_tuple, **kwargs) # fine + f(*args_union, **kwargs) # fine +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 5014349f05fb0..9095aadf18144 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -24,6 +24,7 @@ use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Sig use crate::db::Db; use crate::dunder_all::dunder_all_names; use crate::place::{DefinedPlace, Definedness, Place, known_module_symbol}; +use crate::subscript::PyIndex; use crate::types::call::arguments::{Expansion, is_expandable_type}; use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; use crate::types::diagnostic::{ @@ -3313,6 +3314,14 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { ) -> Result<(), ()> { enum VariadicArgumentType<'db> { ParamSpec(Type<'db>), + /// A union type where each element has been individually iterated into a tuple spec. + /// We pre-compute the per-position union types, length bounds, and variable element + /// so the rest of the matching logic can handle unions without special-casing. + Union { + argument_types: Vec>, + length: TupleLength, + variable_element: Option>, + }, Other(Cow<'db, TupleSpec<'db>>), None, } @@ -3328,13 +3337,86 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { // `*args: P.args` (another ParamSpec). Some(argument_type) => match argument_type.as_paramspec_typevar(db) { Some(paramspec) => VariadicArgumentType::ParamSpec(paramspec), - // TODO: `Type::iterate` internally handles unions, but in a lossy way. - // It might be superior here to manually map over the union and call `try_iterate` - // on each element, similar to the way that `unpacker.rs` does in the `unpack_inner` method. - // It might be a bit of a refactor, though. - // See - // for more details. --Alex - None => VariadicArgumentType::Other(argument_type.iterate(db)), + None => match argument_type { + // `Type::iterate` unions tuple specs in a way that can invent additional + // arities. Iterate each union element individually and compute per-position + // union types, length bounds, and variable element so that the rest of the + // matching logic handles unions correctly. + // + // We restrict this to cases where all remaining positional parameters are + // defaulted and there is no variadic parameter, because the per-position + // union loses the correlation between element lengths and per-position types. + // For example, given overloads `f(x: int, y: int)` and `f(x: int, y: str, z: int)` + // with `t: tuple[int, str] | tuple[int, str, int]`, the per-position union + // would collapse the two arities, preventing the expansion step from correctly + // splitting the union into separate argument lists per overload. + // + // TODO: This is overly conservative. We could also apply this when all + // non-defaulted parameters are covered by the shortest union element, + // e.g. `f(a: int, b: int = 0)` with `*x` where `x: tuple[int] | tuple[int, int]`. + Type::Union(union) + if self.parameters.variadic().is_none() + && self + .parameters + .positional() + .skip(self.next_positional) + .all(|parameter| parameter.default_type().is_some()) => + { + let tuple_specs: Vec<_> = + union.elements(db).iter().map(|ty| ty.iterate(db)).collect(); + + let min_len = tuple_specs + .iter() + .map(|s| s.len().minimum()) + .min() + .unwrap_or(0); + let any_variable = tuple_specs.iter().any(|s| s.len().is_variable()); + let max_elements = tuple_specs + .iter() + .map(|s| s.all_elements().len()) + .max() + .unwrap_or(0); + + let variable_element = { + let var_types: Vec<_> = tuple_specs + .iter() + .filter_map(|s| s.variable_element().copied()) + .collect(); + if var_types.is_empty() { + None + } else { + Some(UnionType::from_elements_leave_aliases(db, var_types)) + } + }; + + let max_elements = i32::try_from(max_elements).unwrap_or(i32::MAX); + let mut argument_types_vec = Vec::new(); + for index in 0..max_elements { + let positional_types: Vec<_> = tuple_specs + .iter() + .filter_map(|s| s.py_index(db, index).ok()) + .collect(); + if positional_types.is_empty() { + break; + } + argument_types_vec + .push(UnionType::from_elements_leave_aliases(db, positional_types)); + } + + let length = if any_variable || argument_types_vec.len() > min_len { + TupleLength::Variable(min_len, 0) + } else { + TupleLength::Fixed(min_len) + }; + + VariadicArgumentType::Union { + argument_types: argument_types_vec, + length, + variable_element, + } + } + _ => VariadicArgumentType::Other(argument_type.iterate(db)), + }, }, None => VariadicArgumentType::None, }; @@ -3343,6 +3425,11 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { VariadicArgumentType::ParamSpec(paramspec) => { ([].as_slice(), TupleLength::unknown(), Some(*paramspec)) } + VariadicArgumentType::Union { + argument_types, + length, + variable_element, + } => (argument_types.as_slice(), *length, *variable_element), VariadicArgumentType::Other(tuple) => ( tuple.all_elements(), tuple.len(), @@ -3352,6 +3439,9 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { }; let mut argument_types = argument_types.iter().copied(); + // This can be true either if we have a true variable-length tuple (in which case + // `variable_element.is_some()`) or if we have a union of different fixed-length tuples (in + // which case `variable_element.is_none()`). let is_variable = length.is_variable(); // We must be able to match up the fixed-length portion of the argument with positional @@ -3367,7 +3457,9 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { // If the tuple is variable-length, we assume that it will soak up all remaining positional // parameters, stopping only when we reach a parameter that has an explicit keyword argument - // or a parameter that can only be provided via keyword argument. + // or a parameter that can only be provided via keyword argument, or if we run out of + // `argument_types` and have no `variable_element`. (The combination of `is_variable` with + // no `variable_element` can only happen with a union of different-fixed-length tuples.) if is_variable { while self .parameters @@ -3380,12 +3472,11 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { { break; } - self.match_positional( - argument_index, - argument, - argument_types.next().or(variable_element), - is_variable, - )?; + let arg_type = argument_types.next().or(variable_element); + if arg_type.is_none() { + break; + } + self.match_positional(argument_index, argument, arg_type, is_variable)?; } } From a546a1102e3c1484de6e3d85df6127124f22d931 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 3 Mar 2026 22:14:45 -0500 Subject: [PATCH 191/261] [ty] Add a diagnostic for an unused awaitable (#23650) ## Summary This is a warning by default. Closes https://github.com/astral-sh/ty/issues/2791. --- crates/ty/docs/rules.md | 237 ++++++++++-------- .../mdtest/diagnostics/unused_awaitable.md | 151 +++++++++++ crates/ty_python_semantic/src/types.rs | 24 ++ .../src/types/diagnostic.rs | 28 +++ .../src/types/infer/builder.rs | 34 ++- ty.schema.json | 10 + 6 files changed, 379 insertions(+), 105 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/diagnostics/unused_awaitable.md diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index a4db6c70ddf15..5f51db2e505b8 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -640,7 +640,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -672,7 +672,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -798,7 +798,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -828,7 +828,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -854,7 +854,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -888,7 +888,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -977,7 +977,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1004,7 +1004,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1032,7 +1032,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1066,7 +1066,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1102,7 +1102,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1126,7 +1126,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1153,7 +1153,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1190,7 +1190,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1222,7 +1222,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1251,7 +1251,7 @@ a: str Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1300,7 +1300,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1344,7 +1344,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1386,7 +1386,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1430,7 +1430,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1468,7 +1468,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1547,7 +1547,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1586,7 +1586,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1647,7 +1647,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1682,7 +1682,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1710,7 +1710,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1744,7 +1744,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1851,7 +1851,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1905,7 +1905,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1935,7 +1935,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1985,7 +1985,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2011,7 +2011,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2042,7 +2042,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2076,7 +2076,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2125,7 +2125,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2154,7 +2154,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2250,7 +2250,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2296,7 +2296,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2323,7 +2323,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2370,7 +2370,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2400,7 +2400,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2430,7 +2430,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2464,7 +2464,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2498,7 +2498,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2529,7 +2529,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2576,7 +2576,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2608,7 +2608,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2643,7 +2643,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2674,7 +2674,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2729,7 +2729,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2772,7 +2772,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2797,7 +2797,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2830,7 +2830,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2859,7 +2859,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2885,7 +2885,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2909,7 +2909,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2942,7 +2942,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2975,7 +2975,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3002,7 +3002,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3029,7 +3029,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3057,7 +3057,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3089,7 +3089,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3126,7 +3126,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3190,7 +3190,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3217,7 +3217,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3249,7 +3249,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3283,7 +3283,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3313,7 +3313,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3342,7 +3342,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3376,7 +3376,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3403,7 +3403,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3431,7 +3431,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3477,7 +3477,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3514,7 +3514,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3538,7 +3538,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3565,7 +3565,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3593,7 +3593,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3651,7 +3651,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3676,7 +3676,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3701,7 +3701,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3740,7 +3740,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3777,7 +3777,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3818,7 +3818,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3840,6 +3840,39 @@ class A: ... A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' ``` +## `unused-awaitable` + + +Default level: warn · +Preview (since 0.0.21) · +Related issues · +View source + + + +**What it does** + +Checks for awaitable objects (such as coroutines) used as expression +statements without being awaited. + +**Why is this bad?** + +Calling an `async def` function returns a coroutine object. If the +coroutine is never awaited, the body of the async function will never +execute, which is almost always a bug. Python emits a +`RuntimeWarning: coroutine was never awaited` at runtime in this case. + +**Examples** + +```python +async def fetch_data() -> str: + return "data" + +async def main() -> None: + fetch_data() # Warning: coroutine is not awaited + await fetch_data() # OK +``` + ## `unused-ignore-comment` @@ -3919,7 +3952,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3982,7 +4015,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unused_awaitable.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unused_awaitable.md new file mode 100644 index 0000000000000..225bd10fb02ce --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unused_awaitable.md @@ -0,0 +1,151 @@ +# Unused awaitable + +## Basic coroutine not awaited + +Calling an `async def` function produces a coroutine that must be awaited. + +```py +async def fetch() -> int: + return 42 + +async def main(): + fetch() # error: [unused-awaitable] +``` + +## Awaited coroutine is fine + +```py +async def fetch() -> int: + return 42 + +async def main(): + await fetch() +``` + +## Assigned coroutine is fine + +```py +async def fetch() -> int: + return 42 + +async def main(): + # TODO: ty should eventually warn about unused coroutines assigned to variables + coro = fetch() +``` + +## Coroutine passed to a function + +When a coroutine is passed as an argument rather than used as an expression statement, no diagnostic +should be emitted. + +```py +async def fetch() -> int: + return 42 + +async def main(): + print(fetch()) +``` + +## Top-level coroutine call + +The lint fires even outside of `async def`, since the coroutine is still discarded. + +```py +async def fetch() -> int: + return 42 + +fetch() # error: [unused-awaitable] +``` + +## Union of awaitables + +When every element of a union is awaitable, the lint should fire. + +```py +from types import CoroutineType +from typing import Any + +def get_coroutine() -> CoroutineType[Any, Any, int] | CoroutineType[Any, Any, str]: + raise NotImplementedError + +async def main(): + get_coroutine() # error: [unused-awaitable] +``` + +## Union with non-awaitable + +When a union contains a non-awaitable element, the lint should not fire. + +```py +from types import CoroutineType +from typing import Any + +def get_maybe_coroutine() -> CoroutineType[Any, Any, int] | int: + raise NotImplementedError + +async def main(): + get_maybe_coroutine() +``` + +## Intersection with awaitable + +When an intersection type contains an awaitable element, the lint should fire. + +```py +from collections.abc import Coroutine +from types import CoroutineType +from ty_extensions import Intersection + +class Foo: ... +class Bar: ... + +def get_coroutine() -> Intersection[Coroutine[Foo, Foo, Foo], CoroutineType[Bar, Bar, Bar]]: + raise NotImplementedError + +async def main(): + get_coroutine() # error: [unused-awaitable] +``` + +## `reveal_type` and `assert_type` are not flagged + +Calls to `reveal_type` and `assert_type` should not trigger this lint, even when their argument is +an awaitable. + +```py +from typing_extensions import assert_type +from types import CoroutineType +from typing import Any + +async def fetch() -> int: + return 42 + +async def main(): + reveal_type(fetch()) # revealed: CoroutineType[Any, Any, int] + assert_type(fetch(), CoroutineType[Any, Any, int]) +``` + +## Non-awaitable expression statement + +Regular non-awaitable expression statements should not trigger this lint. + +```py +def compute() -> int: + return 42 + +def main(): + compute() +``` + +## Dynamic type + +`Any` and `Unknown` types should not trigger the lint. + +```py +from typing import Any + +def get_any() -> Any: + return None + +async def main(): + get_any() +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 088b2ee267516..cb6916ec771e8 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -995,6 +995,30 @@ impl<'db> Type<'db> { self.is_dynamic() && !self.is_divergent() } + /// Returns `true` if this type is an awaitable that should be awaited before being discarded. + /// + /// Currently checks for instances of `types.CoroutineType` (returned by `async def` calls). + /// Unions are considered awaitable only if every element is awaitable. + /// Intersections are considered awaitable if any positive element is awaitable. + pub(crate) fn is_awaitable(self, db: &'db dyn Db) -> bool { + match self { + Type::NominalInstance(instance) => { + matches!(instance.known_class(db), Some(KnownClass::CoroutineType)) + } + Type::Union(union) => { + let elements = union.elements(db); + // Guard against empty unions (`Never`), since `all()` on an empty + // iterator returns `true`. + !elements.is_empty() && elements.iter().all(|ty| ty.is_awaitable(db)) + } + Type::Intersection(intersection) => intersection + .positive(db) + .iter() + .any(|ty| ty.is_awaitable(db)), + _ => false, + } + } + /// Is a value of this type only usable in typing contexts? pub fn is_type_check_only(&self, db: &'db dyn Db) -> bool { match self { diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index d8910b48dcdb5..a44931bef59e2 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -139,6 +139,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&UNSUPPORTED_BASE); registry.register_lint(&UNSUPPORTED_DYNAMIC_BASE); registry.register_lint(&UNSUPPORTED_OPERATOR); + registry.register_lint(&UNUSED_AWAITABLE); registry.register_lint(&ZERO_STEPSIZE_IN_SLICE); registry.register_lint(&STATIC_ASSERT_ERROR); registry.register_lint(&INVALID_ATTRIBUTE_ACCESS); @@ -2686,6 +2687,33 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for awaitable objects (such as coroutines) used as expression + /// statements without being awaited. + /// + /// ## Why is this bad? + /// Calling an `async def` function returns a coroutine object. If the + /// coroutine is never awaited, the body of the async function will never + /// execute, which is almost always a bug. Python emits a + /// `RuntimeWarning: coroutine was never awaited` at runtime in this case. + /// + /// ## Examples + /// ```python + /// async def fetch_data() -> str: + /// return "data" + /// + /// async def main() -> None: + /// fetch_data() # Warning: coroutine is not awaited + /// await fetch_data() # OK + /// ``` + pub(crate) static UNUSED_AWAITABLE = { + summary: "detects awaitable objects that are used as expression statements without being awaited", + status: LintStatus::preview("0.0.21"), + default_level: Level::Warn, + } +} + declare_lint! { /// ## What it does /// Checks for step size 0 in slices. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 88e53da9ebed1..3a356dd8084ab 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -85,8 +85,8 @@ use crate::types::diagnostic::{ POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, - hint_if_stdlib_attribute_exists_on_other_versions, + UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, + USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, report_call_to_abstract_method, report_cannot_delete_typed_dict_key, @@ -561,6 +561,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.in_stub() } + /// Returns `true` if `expr` is a call to a known diagnostic function + /// (e.g., `reveal_type` or `assert_type`) whose return value should not + /// trigger the `unused-awaitable` lint. + fn is_known_function_call(&self, expr: &ast::Expr) -> bool { + let ast::Expr::Call(call) = expr else { + return false; + }; + matches!( + self.expression_type(&call.func), + Type::FunctionLiteral(f) + if matches!( + f.known(self.db()), + Some(KnownFunction::RevealType | KnownFunction::AssertType) + ) + ) + } + /// Get the already-inferred type of an expression node, or Unknown. fn expression_type(&self, expr: &ast::Expr) -> Type<'db> { self.try_expression_type(expr).unwrap_or_else(Type::unknown) @@ -3267,7 +3284,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) => { // If this is a call expression, we would have added a `ReturnsNever` constraint, // meaning this will be a standalone expression. - self.infer_maybe_standalone_expression(value, TypeContext::default()); + let ty = self.infer_maybe_standalone_expression(value, TypeContext::default()); + + if ty.is_awaitable(self.db()) && !self.is_known_function_call(value) { + if let Some(builder) = + self.context.report_lint(&UNUSED_AWAITABLE, value.as_ref()) + { + builder.into_diagnostic(format_args!( + "Object of type `{}` is not awaited", + ty.display(self.db()), + )); + } + } } ast::Stmt::If(if_statement) => self.infer_if_statement(if_statement), ast::Stmt::Try(try_statement) => self.infer_try_statement(try_statement), diff --git a/ty.schema.json b/ty.schema.json index 5e43d4cd15c68..a95d01a2df0b9 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1445,6 +1445,16 @@ } ] }, + "unused-awaitable": { + "title": "detects awaitable objects that are used as expression statements without being awaited", + "description": "## What it does\nChecks for awaitable objects (such as coroutines) used as expression\nstatements without being awaited.\n\n## Why is this bad?\nCalling an `async def` function returns a coroutine object. If the\ncoroutine is never awaited, the body of the async function will never\nexecute, which is almost always a bug. Python emits a\n`RuntimeWarning: coroutine was never awaited` at runtime in this case.\n\n## Examples\n```python\nasync def fetch_data() -> str:\n return \"data\"\n\nasync def main() -> None:\n fetch_data() # Warning: coroutine is not awaited\n await fetch_data() # OK\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "unused-ignore-comment": { "title": "detects unused `ty: ignore` comments", "description": "## What it does\nChecks for `ty: ignore` directives that are no longer applicable.\n\n## Why is this bad?\nA `ty: ignore` directive that no longer matches any diagnostic violations is likely\nincluded by mistake, and should be removed to avoid confusion.\n\n## Examples\n```py\na = 20 / 2 # ty: ignore[division-by-zero]\n```\n\nUse instead:\n\n```py\na = 20 / 2\n```\n\n## Options\nSet [`analysis.respect-type-ignore-comments`](https://docs.astral.sh/ty/reference/configuration/#respect-type-ignore-comments)\nto `false` to prevent this rule from reporting unused `type: ignore` comments.", From 1d123a8be7918cf9960bcc4e2584d2a6ff758dda Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 4 Mar 2026 09:06:22 -0500 Subject: [PATCH 192/261] [ty] Avoid stack overflow with recursive typevar (#23652) ## Summary Use a syntactic check rather than eagerly resolving the type. Closes https://github.com/astral-sh/ty/issues/2889 --- .../mdtest/generics/legacy/classes.md | 3 + .../mdtest/generics/pep695/aliases.md | 39 +++++ .../mdtest/generics/pep695/classes.md | 3 + ...neric\342\200\246_(5a066394f338af48).snap" | 122 +++++++------ crates/ty_python_semantic/src/types.rs | 162 +++++++++++++++--- 5 files changed, 255 insertions(+), 74 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 6601893b3929c..f1ce8d5eb277d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -282,6 +282,9 @@ class WithDefault(Generic[T, WithDefaultU]): ... reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] + +# error: [invalid-type-arguments] "Too many type arguments to class `WithDefault`: expected between 1 and 2, got 3" +reveal_type(WithDefault[str, str, str]()) # revealed: WithDefault[Unknown, Unknown] ``` Type variable defaults can reference earlier type variables, but not later ones: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 5a3140926d70d..b8fb4c04962b6 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -246,6 +246,45 @@ def _(g: G): reveal_type(g) # revealed: list[int] ``` +Self-referential defaults should not crash type inference: + +```py +# error: [cyclic-type-alias-definition] "Cyclic definition of `A`" +type A[T = A] = A[int] +``` + +A self-referential default that does not reference itself in the alias body should also not crash, +even when the default is evaluated (e.g., by omitting the type argument): + +```py +type B[T = B] = list[T] + +def _(x: B) -> None: + pass +``` + +Mutually-referential defaults (where two type aliases reference each other via their typevar +defaults) should also not crash: + +```py +type X[T = Y] = list[T] +type Y[U = X] = list[U] + +def _(x: X, y: Y) -> None: + pass +``` + +Indirect self-references through a chain of type aliases should also not crash: + +```py +type P[T = R] = list[T] +type Q[T = P] = list[T] +type R[T = Q] = list[T] + +def _(p: P) -> None: + pass +``` + ## Snapshots of verbose diagnostics diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index c09f91e804dd1..2eb4f6665994d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -210,6 +210,9 @@ class WithDefault[T, U = int]: ... reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] + +# error: [invalid-type-arguments] "Too many type arguments to class `WithDefault`: expected between 1 and 2, got 3" +reveal_type(WithDefault[str, str, str]()) # revealed: WithDefault[Unknown, Unknown] ``` ## Diagnostics for bad specializations diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" index e92d1159c34d2..eb8400acf3b26 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" @@ -70,31 +70,34 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/classes. 55 | 56 | reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] 57 | reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] -58 | from typing_extensions import TypeVar, Generic -59 | -60 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) -61 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) +58 | +59 | # error: [invalid-type-arguments] "Too many type arguments to class `WithDefault`: expected between 1 and 2, got 3" +60 | reveal_type(WithDefault[str, str, str]()) # revealed: WithDefault[Unknown, Unknown] +61 | from typing_extensions import TypeVar, Generic 62 | -63 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it -64 | class GoodOrder(Generic[WithDefaultT1, WithDefaultT2]): ... +63 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) +64 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) 65 | -66 | # error: [invalid-generic-class] "Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1`" -67 | class BadOrder(Generic[WithDefaultT2, WithDefaultT1]): ... +66 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it +67 | class GoodOrder(Generic[WithDefaultT1, WithDefaultT2]): ... 68 | -69 | WithDefaultU = TypeVar("WithDefaultU", default=int) -70 | -71 | # error: [invalid-generic-class] -72 | class AlsoBadOrder(Generic[WithDefaultT2, WithDefaultT1, WithDefaultU]): ... -73 | from typing_extensions import TypeVar, Generic -74 | -75 | StartT = TypeVar("StartT", default=int) -76 | StopT = TypeVar("StopT", default=StartT) -77 | StepT = TypeVar("StepT", default=int | None) -78 | Start2T = TypeVar("Start2T", default="StopT") -79 | Stop2T = TypeVar("Stop2T", default=int) -80 | -81 | # error: [invalid-generic-class] "Default of `Start2T` cannot reference out-of-scope type variable `StopT`" -82 | class Bad(Generic[Start2T, Stop2T, StepT]): ... +69 | # error: [invalid-generic-class] "Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1`" +70 | class BadOrder(Generic[WithDefaultT2, WithDefaultT1]): ... +71 | +72 | WithDefaultU = TypeVar("WithDefaultU", default=int) +73 | +74 | # error: [invalid-generic-class] +75 | class AlsoBadOrder(Generic[WithDefaultT2, WithDefaultT1, WithDefaultU]): ... +76 | from typing_extensions import TypeVar, Generic +77 | +78 | StartT = TypeVar("StartT", default=int) +79 | StopT = TypeVar("StopT", default=StartT) +80 | StepT = TypeVar("StepT", default=int | None) +81 | Start2T = TypeVar("Start2T", default="StopT") +82 | Stop2T = TypeVar("Stop2T", default=int) +83 | +84 | # error: [invalid-generic-class] "Default of `Start2T` cannot reference out-of-scope type variable `StopT`" +85 | class Bad(Generic[Start2T, Stop2T, StepT]): ... ``` # Diagnostics @@ -179,26 +182,39 @@ info: rule `invalid-type-arguments` is enabled by default ``` +``` +error[invalid-type-arguments]: Too many type arguments to class `WithDefault`: expected between 1 and 2, got 3 + --> src/mdtest_snippet.py:60:35 + | +59 | # error: [invalid-type-arguments] "Too many type arguments to class `WithDefault`: expected between 1 and 2, got 3" +60 | reveal_type(WithDefault[str, str, str]()) # revealed: WithDefault[Unknown, Unknown] + | ^^^ +61 | from typing_extensions import TypeVar, Generic + | +info: rule `invalid-type-arguments` is enabled by default + +``` + ``` error[invalid-generic-class]: Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1` - --> src/mdtest_snippet.py:67:7 + --> src/mdtest_snippet.py:70:7 | -66 | # error: [invalid-generic-class] "Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1`" -67 | class BadOrder(Generic[WithDefaultT2, WithDefaultT1]): ... +69 | # error: [invalid-generic-class] "Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1`" +70 | class BadOrder(Generic[WithDefaultT2, WithDefaultT1]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -68 | -69 | WithDefaultU = TypeVar("WithDefaultU", default=int) +71 | +72 | WithDefaultU = TypeVar("WithDefaultU", default=int) | - ::: src/mdtest_snippet.py:60:1 + ::: src/mdtest_snippet.py:63:1 | -58 | from typing_extensions import TypeVar, Generic -59 | -60 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) +61 | from typing_extensions import TypeVar, Generic +62 | +63 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) | ----------------------------------------------------- `WithDefaultT1` defined here -61 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) +64 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) | --------------------------------------------------------------- `WithDefaultT2` defined here -62 | -63 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it +65 | +66 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it | info: rule `invalid-generic-class` is enabled by default @@ -206,23 +222,23 @@ info: rule `invalid-generic-class` is enabled by default ``` error[invalid-generic-class]: Default of `WithDefaultT2` cannot reference later type parameter `WithDefaultT1` - --> src/mdtest_snippet.py:72:7 + --> src/mdtest_snippet.py:75:7 | -71 | # error: [invalid-generic-class] -72 | class AlsoBadOrder(Generic[WithDefaultT2, WithDefaultT1, WithDefaultU]): ... +74 | # error: [invalid-generic-class] +75 | class AlsoBadOrder(Generic[WithDefaultT2, WithDefaultT1, WithDefaultU]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -73 | from typing_extensions import TypeVar, Generic +76 | from typing_extensions import TypeVar, Generic | - ::: src/mdtest_snippet.py:60:1 + ::: src/mdtest_snippet.py:63:1 | -58 | from typing_extensions import TypeVar, Generic -59 | -60 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) +61 | from typing_extensions import TypeVar, Generic +62 | +63 | WithDefaultT1 = TypeVar("WithDefaultT1", default=int) | ----------------------------------------------------- `WithDefaultT1` defined here -61 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) +64 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) | --------------------------------------------------------------- `WithDefaultT2` defined here -62 | -63 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it +65 | +66 | # This is fine: WithDefaultT2's default references WithDefaultT1, which comes before it | info: rule `invalid-generic-class` is enabled by default @@ -230,19 +246,19 @@ info: rule `invalid-generic-class` is enabled by default ``` error[invalid-generic-class]: Default of `Start2T` cannot reference out-of-scope type variable `StopT` - --> src/mdtest_snippet.py:82:7 + --> src/mdtest_snippet.py:85:7 | -81 | # error: [invalid-generic-class] "Default of `Start2T` cannot reference out-of-scope type variable `StopT`" -82 | class Bad(Generic[Start2T, Stop2T, StepT]): ... +84 | # error: [invalid-generic-class] "Default of `Start2T` cannot reference out-of-scope type variable `StopT`" +85 | class Bad(Generic[Start2T, Stop2T, StepT]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - ::: src/mdtest_snippet.py:78:1 + ::: src/mdtest_snippet.py:81:1 | -76 | StopT = TypeVar("StopT", default=StartT) -77 | StepT = TypeVar("StepT", default=int | None) -78 | Start2T = TypeVar("Start2T", default="StopT") +79 | StopT = TypeVar("StopT", default=StartT) +80 | StepT = TypeVar("StepT", default=int | None) +81 | Start2T = TypeVar("Start2T", default="StopT") | --------------------------------------------- `Start2T` defined here -79 | Stop2T = TypeVar("Stop2T", default=int) +82 | Stop2T = TypeVar("Stop2T", default=int) | info: rule `invalid-generic-class` is enabled by default diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cb6916ec771e8..75197ace94b0d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1,7 +1,7 @@ use compact_str::ToCompactString; use itertools::Itertools; use ruff_diagnostics::{Edit, Fix}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; use std::cell::RefCell; @@ -238,6 +238,11 @@ pub(crate) struct FindLegacyTypeVars; pub(crate) type SpecializationVisitor<'db> = CycleDetector, ()>; pub(crate) struct VisitSpecialization; +/// A [`CycleDetector`] that is used in `TypeVarInstance::default_type`. +pub(crate) type TypeVarDefaultVisitor<'db> = + CycleDetector, Option>>; +pub(crate) struct VisitTypeVarDefault; + /// How a generic type has been specialized. /// /// This matters only if there is at least one invariant type parameter. @@ -7113,9 +7118,20 @@ impl<'db> TypeVarInstance<'db> { } pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { - self._default(db).and_then(|d| match d { - TypeVarDefaultEvaluation::Eager(ty) => Some(ty), - TypeVarDefaultEvaluation::Lazy => self.lazy_default(db), + let visitor = TypeVarDefaultVisitor::new(None); + self.default_type_impl(db, &visitor) + } + + fn default_type_impl( + self, + db: &'db dyn Db, + visitor: &TypeVarDefaultVisitor<'db>, + ) -> Option> { + visitor.visit(self, || { + self._default(db).and_then(|default| match default { + TypeVarDefaultEvaluation::Eager(ty) => Some(ty), + TypeVarDefaultEvaluation::Lazy => self.lazy_default_impl(db, visitor), + }) }) } @@ -7186,15 +7202,87 @@ impl<'db> TypeVarInstance<'db> { )) } - fn type_is_self_referential(self, db: &'db dyn Db, ty: Type<'db>) -> bool { - let identity = self.identity(db); - any_over_type(db, ty, false, |ty| match ty { - Type::TypeVar(bound_typevar) => identity == bound_typevar.typevar(db).identity(db), - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { - identity == typevar.identity(db) + fn type_is_self_referential( + self, + db: &'db dyn Db, + ty: Type<'db>, + visitor: &TypeVarDefaultVisitor<'db>, + ) -> bool { + #[derive(Copy, Clone)] + struct State<'db, 'a> { + db: &'db dyn Db, + visitor: &'a TypeVarDefaultVisitor<'db>, + seen_typevars: &'a RefCell>>, + seen_type_aliases: &'a RefCell>>, + } + + fn typevar_default_is_self_referential<'db>( + state: State<'db, '_>, + typevar: TypeVarInstance<'db>, + self_identity: TypeVarIdentity<'db>, + ) -> bool { + if typevar.identity(state.db) == self_identity { + return true; } - _ => false, - }) + + if !state.seen_typevars.borrow_mut().insert(typevar) { + return false; + } + + typevar + .default_type_impl(state.db, state.visitor) + .is_some_and(|default_ty| { + type_is_self_referential_impl(state, default_ty, self_identity) + }) + } + + fn type_alias_is_self_referential<'db>( + state: State<'db, '_>, + type_alias: TypeAliasType<'db>, + self_identity: TypeVarIdentity<'db>, + ) -> bool { + if !state.seen_type_aliases.borrow_mut().insert(type_alias) { + return false; + } + + type_is_self_referential_impl(state, type_alias.raw_value_type(state.db), self_identity) + } + + fn type_is_self_referential_impl<'db>( + state: State<'db, '_>, + ty: Type<'db>, + self_identity: TypeVarIdentity<'db>, + ) -> bool { + any_over_type(state.db, ty, false, |inner_ty| match inner_ty { + Type::TypeVar(bound_typevar) => typevar_default_is_self_referential( + state, + bound_typevar.typevar(state.db), + self_identity, + ), + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { + typevar_default_is_self_referential(state, typevar, self_identity) + } + Type::TypeAlias(alias) => { + type_alias_is_self_referential(state, alias, self_identity) + } + Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => { + type_alias_is_self_referential(state, alias, self_identity) + } + _ => false, + }) + } + + let seen_typevars = RefCell::new(FxHashSet::default()); + let seen_type_aliases = RefCell::new(FxHashSet::default()); + + let state = State { + db, + visitor, + seen_typevars: &seen_typevars, + seen_type_aliases: &seen_type_aliases, + }; + + type_is_self_referential_impl(state, ty, self.identity(db)) } /// Returns the "unchecked" upper bound of a type variable instance. @@ -7375,12 +7463,21 @@ impl<'db> TypeVarInstance<'db> { } fn lazy_default(self, db: &'db dyn Db) -> Option> { + let visitor = TypeVarDefaultVisitor::new(None); + self.lazy_default_impl(db, &visitor) + } + + fn lazy_default_impl( + self, + db: &'db dyn Db, + visitor: &TypeVarDefaultVisitor<'db>, + ) -> Option> { let default = self.lazy_default_unchecked(db)?; // Unlike bounds/constraints, default types are allowed to be generic (https://peps.python.org/pep-0696/#using-another-type-parameter-as-default). // Here we simply check for non-self-referential. // TODO: We should also check for non-forward references. - if self.type_is_self_referential(db, default) { + if self.type_is_self_referential(db, default, visitor) { return None; } @@ -7804,14 +7901,7 @@ impl<'db> BoundTypeVarInstance<'db> { /// `BoundTypeVarInstance`. As part of binding `U` we must also bind its default value /// (resulting in `T@C`). pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { - let binding_context = self.binding_context(db); - self.typevar(db).default_type(db).map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::BindLegacyTypevars(binding_context), - TypeContext::default(), - ) - }) + bound_typevar_default_type(db, self) } fn materialize_impl( @@ -7839,6 +7929,36 @@ impl<'db> BoundTypeVarInstance<'db> { } } +#[salsa::tracked( + cycle_initial=|_, _, _| None, + cycle_fn=bound_typevar_default_type_cycle_recover, + heap_size=ruff_memory_usage::heap_size +)] +fn bound_typevar_default_type<'db>( + db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, +) -> Option> { + let binding_context = bound_typevar.binding_context(db); + bound_typevar.typevar(db).default_type(db).map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::BindLegacyTypevars(binding_context), + TypeContext::default(), + ) + }) +} + +#[expect(clippy::ref_option)] +fn bound_typevar_default_type_cycle_recover<'db>( + _db: &'db dyn Db, + _cycle: &salsa::Cycle, + _previous_default: &Option>, + _default: Option>, + _bound_typevar: BoundTypeVarInstance<'db>, +) -> Option> { + None +} + /// Whether a typevar default is eagerly specified or lazily evaluated. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub enum TypeVarDefaultEvaluation<'db> { From 04023a2a65ebcde99a3bc2f4d0644092581a65ba Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 4 Mar 2026 14:57:19 +0000 Subject: [PATCH 193/261] [ty] Move `CallableType`, and related methods/types, to a new `types::callable` submodule (#23707) --- crates/ty_python_semantic/src/types.rs | 458 +---------------- .../ty_python_semantic/src/types/callable.rs | 472 ++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 5 +- .../src/types/protocol_class.rs | 3 +- .../src/types/signatures.rs | 3 +- .../ty_python_semantic/src/types/visitor.rs | 7 +- 6 files changed, 487 insertions(+), 461 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/callable.rs diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 75197ace94b0d..12d4b2ea5f22c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -18,7 +18,7 @@ use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; use ruff_text_size::Ranged; -use smallvec::{SmallVec, smallvec_inline}; +use smallvec::smallvec_inline; use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; pub(crate) use self::class::DynamicClassLiteral; @@ -51,10 +51,9 @@ use crate::semantic_index::{imported_modules, place_table, semantic_index}; use crate::suppression::check_suppressions; use crate::types::bound_super::BoundSuperType; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; +pub(crate) use crate::types::callable::{CallableType, CallableTypeKind, CallableTypes}; pub(crate) use crate::types::class_base::ClassBase; -use crate::types::constraints::{ - ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, -}; +use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM}; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; @@ -96,6 +95,7 @@ pub use special_form::SpecialFormType; mod bool; mod bound_super; mod call; +mod callable; mod class; mod class_base; mod constraints; @@ -447,7 +447,6 @@ macro_rules! todo_type { } pub use crate::types::definition::TypeDefinition; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; pub(crate) use todo_type; /// Represents an instance of `builtins.property`. @@ -1866,163 +1865,6 @@ impl<'db> Type<'db> { } } - pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { - match self { - Type::Callable(callable) => Some(CallableTypes::one(callable)), - - Type::Dynamic(_) => Some(CallableTypes::one(CallableType::function_like( - db, - Signature::dynamic(self), - ))), - - Type::FunctionLiteral(function_literal) => { - Some(CallableTypes::one(function_literal.into_callable_type(db))) - } - Type::BoundMethod(bound_method) => { - Some(CallableTypes::one(bound_method.into_callable_type(db))) - } - - Type::NominalInstance(_) | Type::ProtocolInstance(_) => { - let call_symbol = self - .member_lookup_with_policy( - db, - Name::new_static("__call__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place; - - if let Place::Defined(place) = call_symbol - && place.is_definitely_defined() - { - place.ty.try_upcast_to_callable(db) - } else { - None - } - } - Type::ClassLiteral(class_literal) => { - Some(class_literal.identity_specialization(db).into_callable(db)) - } - - Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)), - - Type::NewTypeInstance(newtype) => { - newtype.concrete_base_type(db).try_upcast_to_callable(db) - } - - // TODO: This is unsound so in future we can consider an opt-in option to disable it. - Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - SubclassOfInner::Class(class) => Some(class.into_callable(db)), - SubclassOfInner::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - let upcast_callables = bound.to_meta_type(db).try_upcast_to_callable(db)?; - Some(upcast_callables.map(|callable| { - let signatures = callable - .signatures(db) - .into_iter() - .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); - CallableType::new( - db, - CallableSignature::from_overloads(signatures), - callable.kind(db), - ) - })) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut callables = SmallVec::new(); - for constraint in constraints.elements(db) { - let element_upcast = - constraint.to_meta_type(db).try_upcast_to_callable(db)?; - for callable in element_upcast.into_inner() { - let signatures = callable - .signatures(db) - .into_iter() - .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); - callables.push(CallableType::new( - db, - CallableSignature::from_overloads(signatures), - callable.kind(db), - )); - } - } - Some(CallableTypes(callables)) - } - None => Some(CallableTypes::one(CallableType::single( - db, - Signature::new(Parameters::gradual_form(), Type::TypeVar(tvar)), - ))), - }, - SubclassOfInner::Dynamic(_) => Some(CallableTypes::one(CallableType::single( - db, - Signature::new(Parameters::unknown(), Type::from(subclass_of_ty)), - ))), - }, - - Type::Union(union) => { - let mut callables = SmallVec::new(); - for element in union.elements(db) { - let element_callable = element.try_upcast_to_callable(db)?; - callables.extend(element_callable.into_inner()); - } - Some(CallableTypes(callables)) - } - - Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::Enum(enum_literal) => enum_literal - .enum_class_instance(db) - .try_upcast_to_callable(db), - _ => None, - }, - - Type::TypeAlias(alias) => alias.value_type(db).try_upcast_to_callable(db), - - Type::KnownBoundMethod(method) => Some(CallableTypes::one(CallableType::new( - db, - CallableSignature::from_overloads(method.signatures(db)), - CallableTypeKind::Regular, - ))), - - Type::WrapperDescriptor(wrapper_descriptor) => { - Some(CallableTypes::one(CallableType::new( - db, - CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), - CallableTypeKind::Regular, - ))) - } - - Type::KnownInstance(KnownInstanceType::NewType(newtype)) => { - Some(CallableTypes::one(CallableType::single( - db, - Signature::new( - Parameters::new( - db, - [Parameter::positional_only(None) - .with_annotated_type(newtype.base(db).instance_type(db))], - ), - Type::NewTypeInstance(newtype), - ), - ))) - } - - Type::Never - | Type::DataclassTransformer(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::TypeIs(_) - | Type::TypeGuard(_) - | Type::TypedDict(_) => None, - - // TODO - Type::DataclassDecorator(_) - | Type::ModuleLiteral(_) - | Type::SpecialForm(_) - | Type::KnownInstance(_) - | Type::PropertyInstance(_) - | Type::Intersection(_) - | Type::TypeVar(_) - | Type::BoundSuper(_) => None, - } - } - /// Recursively visit the specialization of a generic class instance. /// /// The provided closure will be called with each assignment of a type variable present in this @@ -8343,298 +8185,6 @@ impl From for Truthiness { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] -pub enum CallableTypeKind { - /// Represents regular callable objects. - Regular, - - /// Represents function-like objects, like the synthesized methods of dataclasses or - /// `NamedTuples`. These callables act like real functions when accessed as attributes on - /// instances, i.e. they bind `self`. - FunctionLike, - - /// A callable type that represents a staticmethod. These callables do not bind `self` - /// when accessed as attributes on instances - they return the underlying function as-is. - StaticMethodLike, - - /// A callable type that we believe represents a classmethod (i.e. it will unconditionally bind - /// the first argument on `__get__`). - ClassMethodLike, - - /// Represents the value bound to a `typing.ParamSpec` type variable. - ParamSpecValue, -} - -/// This type represents the set of all callable objects with a certain, possibly overloaded, -/// signature. -/// -/// It can be written in type expressions using `typing.Callable`. `lambda` expressions are -/// inferred directly as `CallableType`s; all function-literal types are subtypes of a -/// `CallableType`. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct CallableType<'db> { - #[returns(ref)] - pub(crate) signatures: CallableSignature<'db>, - - kind: CallableTypeKind, -} - -pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - ty: CallableType<'db>, - visitor: &V, -) { - for signature in &ty.signatures(db).overloads { - walk_signature(db, signature, visitor); - } -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for CallableType<'_> {} - -impl<'db> Type<'db> { - /// Create a callable type with a single non-overloaded signature. - pub(crate) fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { - Type::Callable(CallableType::single(db, signature)) - } - - /// Create a non-overloaded, function-like callable type with a single signature. - /// - /// A function-like callable will bind `self` when accessed as an attribute on an instance. - pub(crate) fn function_like_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { - Type::Callable(CallableType::function_like(db, signature)) - } - - /// Create a non-overloaded callable type which represents the value bound to a `ParamSpec` - /// type variable. - pub(crate) fn paramspec_value_callable( - db: &'db dyn Db, - parameters: Parameters<'db>, - ) -> Type<'db> { - Type::Callable(CallableType::paramspec_value(db, parameters)) - } -} - -impl<'db> CallableType<'db> { - pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { - CallableType::new( - db, - CallableSignature::single(signature), - CallableTypeKind::Regular, - ) - } - - pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { - CallableType::new( - db, - CallableSignature::single(signature), - CallableTypeKind::FunctionLike, - ) - } - - pub(crate) fn paramspec_value( - db: &'db dyn Db, - parameters: Parameters<'db>, - ) -> CallableType<'db> { - CallableType::new( - db, - CallableSignature::single(Signature::new(parameters, Type::unknown())), - CallableTypeKind::ParamSpecValue, - ) - } - - /// Create a callable type which accepts any parameters and returns an `Unknown` type. - pub(crate) fn unknown(db: &'db dyn Db) -> CallableType<'db> { - Self::single(db, Signature::unknown()) - } - - pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool { - matches!(self.kind(db), CallableTypeKind::FunctionLike) - } - - pub(crate) fn is_classmethod_like(self, db: &'db dyn Db) -> bool { - matches!(self.kind(db), CallableTypeKind::ClassMethodLike) - } - - pub(crate) fn is_staticmethod_like(self, db: &'db dyn Db) -> bool { - matches!(self.kind(db), CallableTypeKind::StaticMethodLike) - } - - pub(crate) fn bind_self( - self, - db: &'db dyn Db, - self_type: Option>, - ) -> CallableType<'db> { - CallableType::new( - db, - self.signatures(db).bind_self(db, self_type), - self.kind(db), - ) - } - - pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> { - CallableType::new( - db, - self.signatures(db).apply_self(db, self_type), - self.kind(db), - ) - } - - /// Create a callable type which represents a fully-static "bottom" callable. - /// - /// Specifically, this represents a callable type with a single signature: - /// `(*args: object, **kwargs: object) -> Never`. - pub(crate) fn bottom(db: &'db dyn Db) -> CallableType<'db> { - Self::new(db, CallableSignature::bottom(), CallableTypeKind::Regular) - } - - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - Some(CallableType::new( - db, - self.signatures(db) - .recursive_type_normalized_impl(db, div, nested)?, - self.kind(db), - )) - } - - fn apply_type_mapping_impl<'a>( - self, - db: &'db dyn Db, - type_mapping: &TypeMapping<'a, 'db>, - tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, - ) -> Self { - if let TypeMapping::RescopeReturnCallables(replacements) = type_mapping { - return replacements.get(&self).copied().unwrap_or(self); - } - - CallableType::new( - db, - self.signatures(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), - self.kind(db), - ) - } - - fn find_legacy_typevars_impl( - self, - db: &'db dyn Db, - binding_context: Option>, - typevars: &mut FxOrderSet>, - visitor: &FindLegacyTypeVarsVisitor<'db>, - ) { - self.signatures(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); - } - - /// Check whether this callable type has the given relation to another callable type. - /// - /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. - #[expect(clippy::too_many_arguments)] - fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - if other.is_function_like(db) && !self.is_function_like(db) { - return ConstraintSet::from_bool(constraints, false); - } - - self.signatures(db).has_relation_to_impl( - db, - other.signatures(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } -} - -/// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually, -/// when coercing that result to a single type, you'll get a `UnionType`. But this lets you handle -/// that result as a list of `CallableType`s before merging them into a `UnionType` should that be -/// helpful. -/// -/// Note that this type is guaranteed to contain at least one callable. If you need to support "no -/// callables" as a possibility, use `Option`. -#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update)] -pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>); - -impl<'db> CallableTypes<'db> { - pub(crate) fn one(callable: CallableType<'db>) -> Self { - CallableTypes(smallvec_inline![callable]) - } - - pub(crate) fn from_elements(callables: impl IntoIterator>) -> Self { - let callables: SmallVec<_> = callables.into_iter().collect(); - assert!(!callables.is_empty(), "CallableTypes should not be empty"); - CallableTypes(callables) - } - - pub(crate) fn exactly_one(self) -> Option> { - match self.0.as_slice() { - [single] => Some(*single), - _ => None, - } - } - - fn as_slice(&self) -> &[CallableType<'db>] { - &self.0 - } - - fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { - self.0 - } - - pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { - match self.0.as_slice() { - [] => unreachable!("CallableTypes should not be empty"), - [single] => Type::Callable(*single), - slice => UnionType::from_elements(db, slice.iter().copied().map(Type::Callable)), - } - } - - pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self { - Self::from_elements(self.0.iter().map(|element| f(*element))) - } - - #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: CallableType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - self.0.iter().when_all(db, constraints, |element| { - element.has_relation_to_impl( - db, - other, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } -} - #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct ModuleLiteralType<'db> { /// The imported module. diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs new file mode 100644 index 0000000000000..99a4f7409c827 --- /dev/null +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -0,0 +1,472 @@ +use ruff_python_ast::name::Name; +use smallvec::{SmallVec, smallvec_inline}; + +use crate::{ + Db, FxOrderSet, + place::Place, + semantic_index::definition::Definition, + types::{ + ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableSignature, ClassType, + FindLegacyTypeVarsVisitor, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, + Parameter, Parameters, Signature, SubclassOfInner, Type, TypeContext, TypeMapping, + TypeVarBoundOrConstraints, UnionType, + constraints::{ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension}, + generics::InferableTypeVars, + relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}, + visitor, walk_signature, + }, +}; + +impl<'db> Type<'db> { + /// Create a callable type with a single non-overloaded signature. + pub(crate) fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + Type::Callable(CallableType::single(db, signature)) + } + + /// Create a non-overloaded, function-like callable type with a single signature. + /// + /// A function-like callable will bind `self` when accessed as an attribute on an instance. + pub(crate) fn function_like_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + Type::Callable(CallableType::function_like(db, signature)) + } + + /// Create a non-overloaded callable type which represents the value bound to a `ParamSpec` + /// type variable. + pub(crate) fn paramspec_value_callable( + db: &'db dyn Db, + parameters: Parameters<'db>, + ) -> Type<'db> { + Type::Callable(CallableType::paramspec_value(db, parameters)) + } + + pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { + match self { + Type::Callable(callable) => Some(CallableTypes::one(callable)), + + Type::Dynamic(_) => Some(CallableTypes::one(CallableType::function_like( + db, + Signature::dynamic(self), + ))), + + Type::FunctionLiteral(function_literal) => { + Some(CallableTypes::one(function_literal.into_callable_type(db))) + } + Type::BoundMethod(bound_method) => { + Some(CallableTypes::one(bound_method.into_callable_type(db))) + } + + Type::NominalInstance(_) | Type::ProtocolInstance(_) => { + let call_symbol = self + .member_lookup_with_policy( + db, + Name::new_static("__call__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place; + + if let Place::Defined(place) = call_symbol + && place.is_definitely_defined() + { + place.ty.try_upcast_to_callable(db) + } else { + None + } + } + Type::ClassLiteral(class_literal) => { + Some(class_literal.identity_specialization(db).into_callable(db)) + } + + Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)), + + Type::NewTypeInstance(newtype) => { + newtype.concrete_base_type(db).try_upcast_to_callable(db) + } + + // TODO: This is unsound so in future we can consider an opt-in option to disable it. + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + SubclassOfInner::Class(class) => Some(class.into_callable(db)), + SubclassOfInner::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + let upcast_callables = bound.to_meta_type(db).try_upcast_to_callable(db)?; + Some(upcast_callables.map(|callable| { + let signatures = callable + .signatures(db) + .into_iter() + .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); + CallableType::new( + db, + CallableSignature::from_overloads(signatures), + callable.kind(db), + ) + })) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + let mut callables = SmallVec::new(); + for constraint in constraints.elements(db) { + let element_upcast = + constraint.to_meta_type(db).try_upcast_to_callable(db)?; + for callable in element_upcast.into_inner() { + let signatures = callable + .signatures(db) + .into_iter() + .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); + callables.push(CallableType::new( + db, + CallableSignature::from_overloads(signatures), + callable.kind(db), + )); + } + } + Some(CallableTypes::new(callables)) + } + None => Some(CallableTypes::one(CallableType::single( + db, + Signature::new(Parameters::gradual_form(), Type::TypeVar(tvar)), + ))), + }, + SubclassOfInner::Dynamic(_) => Some(CallableTypes::one(CallableType::single( + db, + Signature::new(Parameters::unknown(), Type::from(subclass_of_ty)), + ))), + }, + + Type::Union(union) => { + let mut callables = SmallVec::new(); + for element in union.elements(db) { + let element_callable = element.try_upcast_to_callable(db)?; + callables.extend(element_callable.into_inner()); + } + Some(CallableTypes::new(callables)) + } + + Type::LiteralValue(literal) => match literal.kind() { + LiteralValueTypeKind::Enum(enum_literal) => enum_literal + .enum_class_instance(db) + .try_upcast_to_callable(db), + _ => None, + }, + + Type::TypeAlias(alias) => alias.value_type(db).try_upcast_to_callable(db), + + Type::KnownBoundMethod(method) => Some(CallableTypes::one(CallableType::new( + db, + CallableSignature::from_overloads(method.signatures(db)), + CallableTypeKind::Regular, + ))), + + Type::WrapperDescriptor(wrapper_descriptor) => { + Some(CallableTypes::one(CallableType::new( + db, + CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), + CallableTypeKind::Regular, + ))) + } + + Type::KnownInstance(KnownInstanceType::NewType(newtype)) => { + Some(CallableTypes::one(CallableType::single( + db, + Signature::new( + Parameters::new( + db, + [Parameter::positional_only(None) + .with_annotated_type(newtype.base(db).instance_type(db))], + ), + Type::NewTypeInstance(newtype), + ), + ))) + } + + Type::Never + | Type::DataclassTransformer(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::TypeIs(_) + | Type::TypeGuard(_) + | Type::TypedDict(_) => None, + + // TODO + Type::DataclassDecorator(_) + | Type::ModuleLiteral(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::Intersection(_) + | Type::TypeVar(_) + | Type::BoundSuper(_) => None, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum CallableTypeKind { + /// Represents regular callable objects. + Regular, + + /// Represents function-like objects, like the synthesized methods of dataclasses or + /// `NamedTuples`. These callables act like real functions when accessed as attributes on + /// instances, i.e. they bind `self`. + FunctionLike, + + /// A callable type that represents a staticmethod. These callables do not bind `self` + /// when accessed as attributes on instances - they return the underlying function as-is. + StaticMethodLike, + + /// A callable type that we believe represents a classmethod (i.e. it will unconditionally bind + /// the first argument on `__get__`). + ClassMethodLike, + + /// Represents the value bound to a `typing.ParamSpec` type variable. + ParamSpecValue, +} + +/// This type represents the set of all callable objects with a certain, possibly overloaded, +/// signature. +/// +/// It can be written in type expressions using `typing.Callable`. `lambda` expressions are +/// inferred directly as `CallableType`s; all function-literal types are subtypes of a +/// `CallableType`. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct CallableType<'db> { + #[returns(ref)] + pub(crate) signatures: CallableSignature<'db>, + + pub(super) kind: CallableTypeKind, +} + +pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + ty: CallableType<'db>, + visitor: &V, +) { + for signature in &ty.signatures(db).overloads { + walk_signature(db, signature, visitor); + } +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for CallableType<'_> {} + +impl<'db> CallableType<'db> { + pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { + CallableType::new( + db, + CallableSignature::single(signature), + CallableTypeKind::Regular, + ) + } + + pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { + CallableType::new( + db, + CallableSignature::single(signature), + CallableTypeKind::FunctionLike, + ) + } + + pub(crate) fn paramspec_value( + db: &'db dyn Db, + parameters: Parameters<'db>, + ) -> CallableType<'db> { + CallableType::new( + db, + CallableSignature::single(Signature::new(parameters, Type::unknown())), + CallableTypeKind::ParamSpecValue, + ) + } + + /// Create a callable type which accepts any parameters and returns an `Unknown` type. + pub(crate) fn unknown(db: &'db dyn Db) -> CallableType<'db> { + Self::single(db, Signature::unknown()) + } + + pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool { + matches!(self.kind(db), CallableTypeKind::FunctionLike) + } + + pub(crate) fn is_classmethod_like(self, db: &'db dyn Db) -> bool { + matches!(self.kind(db), CallableTypeKind::ClassMethodLike) + } + + pub(crate) fn is_staticmethod_like(self, db: &'db dyn Db) -> bool { + matches!(self.kind(db), CallableTypeKind::StaticMethodLike) + } + + pub(crate) fn bind_self( + self, + db: &'db dyn Db, + self_type: Option>, + ) -> CallableType<'db> { + CallableType::new( + db, + self.signatures(db).bind_self(db, self_type), + self.kind(db), + ) + } + + pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> { + CallableType::new( + db, + self.signatures(db).apply_self(db, self_type), + self.kind(db), + ) + } + + /// Create a callable type which represents a fully-static "bottom" callable. + /// + /// Specifically, this represents a callable type with a single signature: + /// `(*args: object, **kwargs: object) -> Never`. + pub(crate) fn bottom(db: &'db dyn Db) -> CallableType<'db> { + Self::new(db, CallableSignature::bottom(), CallableTypeKind::Regular) + } + + pub(super) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + Some(CallableType::new( + db, + self.signatures(db) + .recursive_type_normalized_impl(db, div, nested)?, + self.kind(db), + )) + } + + pub(super) fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + tcx: TypeContext<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + if let TypeMapping::RescopeReturnCallables(replacements) = type_mapping { + return replacements.get(&self).copied().unwrap_or(self); + } + + CallableType::new( + db, + self.signatures(db) + .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + self.kind(db), + ) + } + + pub(super) fn find_legacy_typevars_impl( + self, + db: &'db dyn Db, + binding_context: Option>, + typevars: &mut FxOrderSet>, + visitor: &FindLegacyTypeVarsVisitor<'db>, + ) { + self.signatures(db) + .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + } + + /// Check whether this callable type has the given relation to another callable type. + /// + /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. + #[expect(clippy::too_many_arguments)] + pub(super) fn has_relation_to_impl<'c>( + self, + db: &'db dyn Db, + other: Self, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + if other.is_function_like(db) && !self.is_function_like(db) { + return ConstraintSet::from_bool(constraints, false); + } + + self.signatures(db).has_relation_to_impl( + db, + other.signatures(db), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + } +} + +/// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually, +/// when coercing that result to a single type, you'll get a `UnionType`. But this lets you handle +/// that result as a list of `CallableType`s before merging them into a `UnionType` should that be +/// helpful. +/// +/// Note that this type is guaranteed to contain at least one callable. If you need to support "no +/// callables" as a possibility, use `Option`. +#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update)] +pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>); + +impl<'db> CallableTypes<'db> { + pub(super) fn new(callables: SmallVec<[CallableType<'db>; 1]>) -> Self { + assert!(!callables.is_empty(), "CallableTypes should not be empty"); + CallableTypes(callables) + } + + pub(crate) fn one(callable: CallableType<'db>) -> Self { + CallableTypes(smallvec_inline![callable]) + } + + pub(crate) fn from_elements(callables: impl IntoIterator>) -> Self { + let callables: SmallVec<_> = callables.into_iter().collect(); + assert!(!callables.is_empty(), "CallableTypes should not be empty"); + CallableTypes(callables) + } + + pub(crate) fn exactly_one(self) -> Option> { + match self.0.as_slice() { + [single] => Some(*single), + _ => None, + } + } + + pub(super) fn as_slice(&self) -> &[CallableType<'db>] { + &self.0 + } + + pub(super) fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { + self.0 + } + + pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { + match self.0.as_slice() { + [] => unreachable!("CallableTypes should not be empty"), + [single] => Type::Callable(*single), + slice => UnionType::from_elements(db, slice.iter().copied().map(Type::Callable)), + } + } + + pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self { + Self::from_elements(self.0.iter().map(|element| f(*element))) + } + + #[expect(clippy::too_many_arguments)] + pub(crate) fn has_relation_to_impl<'c>( + self, + db: &'db dyn Db, + other: CallableType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + self.0.iter().when_all(db, constraints, |element| { + element.has_relation_to_impl( + db, + other, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } +} diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 119e4b73e4ddf..d34195041af4a 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -12,6 +12,7 @@ use crate::node_key::NodeKey; use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKey, NodeWithScopeKind, ScopeId}; use crate::semantic_index::{SemanticIndex, semantic_index}; +use crate::types::callable::walk_callable_type; use crate::types::class::ClassType; use crate::types::class_base::ClassBase; use crate::types::constraints::{ @@ -27,8 +28,8 @@ use crate::types::{ CallableType, CallableTypes, ClassLiteral, FindLegacyTypeVarsVisitor, IntersectionType, KnownClass, KnownInstanceType, MaterializationKind, Type, TypeAliasType, TypeContext, TypeMapping, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, - TypeVarVariance, UnionType, declaration_type, walk_callable_type, - walk_manual_pep_695_type_alias, walk_pep_695_type_alias, walk_type_var_bounds, + TypeVarVariance, UnionType, declaration_type, walk_manual_pep_695_type_alias, + walk_pep_695_type_alias, walk_type_var_bounds, }; use crate::{Db, FxIndexMap, FxOrderMap, FxOrderSet}; diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 44d643d6610df..87d3ea7066821 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -6,8 +6,9 @@ use itertools::Itertools; use ruff_python_ast::name::Name; use rustc_hash::FxHashMap; +use crate::types::TypeContext; +use crate::types::callable::CallableTypeKind; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; -use crate::types::{CallableTypeKind, TypeContext}; use crate::{ Db, FxOrderSet, place::{ diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 4ba5644671e26..33a9cc662d9ef 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -18,6 +18,7 @@ use smallvec::{SmallVec, smallvec_inline}; use super::{DynamicType, Type, TypeVarVariance, semantic_index}; use crate::semantic_index::definition::Definition; +use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; @@ -25,7 +26,7 @@ use crate::types::generics::{GenericContext, InferableTypeVars, walk_generic_con use crate::types::infer::infer_deferred_types; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::{ - ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypeKind, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, ParamSpecAttrKind, SelfBinding, TypeContext, TypeMapping, UnionBuilder, VarianceInferable, infer_complete_scope_types, todo_type, diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 50847a0eb9304..4819477fe7007 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -8,6 +8,7 @@ use crate::{ PropertyInstanceType, ProtocolInstanceType, SubclassOfType, Type, TypeAliasType, TypeGuardType, TypeIsType, TypeVarInstance, TypedDictType, UnionType, bound_super::walk_bound_super_type, + callable::walk_callable_type, class::walk_generic_alias, function::{FunctionType, walk_function_type}, instance::{walk_nominal_instance_type, walk_protocol_instance_type}, @@ -15,9 +16,9 @@ use crate::{ method::{walk_bound_method_type, walk_method_wrapper_type}, newtype::{NewType, walk_newtype_instance_type}, subclass_of::walk_subclass_of_type, - walk_bound_type_var_type, walk_callable_type, walk_intersection_type, - walk_property_instance_type, walk_type_alias_type, walk_type_var_type, - walk_typed_dict_type, walk_typeguard_type, walk_typeis_type, walk_union, + walk_bound_type_var_type, walk_intersection_type, walk_property_instance_type, + walk_type_alias_type, walk_type_var_type, walk_typed_dict_type, walk_typeguard_type, + walk_typeis_type, walk_union, }, }; use std::cell::{Cell, RefCell}; From b910e7ddfc353e3d39e87aaabd3d1ce339c02fe5 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 4 Mar 2026 07:46:37 -0500 Subject: [PATCH 194/261] [ty] Fix handling of non-Python text documents In #22449, I added a check to our "did open" handler to effectively ignore notifications for text documents that we were sure weren't Python. This was meant to fix a case where we could return diagnostics for non-Python files, which was undesirable. However, it seems like that might have been too big of a hammer. It seems like we might still want to track non-Python text files in our index but not our project. Otherwise subsequent requests regarding that non-Python file result in log messages saying that ty doesn't know about the file. i.e., a state synchronization issue. Addresses https://github.com/astral-sh/ruff/pull/23121#discussion_r2882639788 --- .../ty_server/src/document/text_document.rs | 15 +++++-------- .../src/server/api/notifications/did_open.rs | 7 +----- .../api/notifications/did_open_notebook.rs | 6 ++--- crates/ty_server/src/session.rs | 22 +++++++++++++++---- crates/ty_server/src/session/index.rs | 14 +++++++----- crates/ty_server/src/system.rs | 2 +- .../tests/e2e/publish_diagnostics.rs | 2 ++ ..._language_of_file_without_extension-3.snap | 21 ++++++++++++++++++ 8 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension-3.snap diff --git a/crates/ty_server/src/document/text_document.rs b/crates/ty_server/src/document/text_document.rs index d8f43aefe42df..90a07185a30fc 100644 --- a/crates/ty_server/src/document/text_document.rs +++ b/crates/ty_server/src/document/text_document.rs @@ -24,7 +24,7 @@ pub struct TextDocument { version: DocumentVersion, /// The language ID of the document as provided by the client. - language_id: Option, + language_id: LanguageId, /// For cells, the path to the notebook document. notebook: Option, @@ -46,22 +46,16 @@ impl From<&str> for LanguageId { } impl TextDocument { - pub fn new(url: Url, contents: String, version: DocumentVersion) -> Self { + pub fn new(url: Url, contents: String, version: DocumentVersion, language_id: &str) -> Self { Self { url, contents, version, - language_id: None, + language_id: LanguageId::from(language_id), notebook: None, } } - #[must_use] - pub fn with_language_id(mut self, language_id: &str) -> Self { - self.language_id = Some(LanguageId::from(language_id)); - self - } - #[must_use] pub(crate) fn with_notebook(mut self, notebook: AnySystemPath) -> Self { self.notebook = Some(notebook); @@ -84,7 +78,7 @@ impl TextDocument { self.version } - pub fn language_id(&self) -> Option { + pub fn language_id(&self) -> LanguageId { self.language_id } @@ -177,6 +171,7 @@ def interface(): "# .to_string(), 0, + "python", ); // Add an `s`, remove it again (back to the original code), and then re-add the `s` diff --git a/crates/ty_server/src/server/api/notifications/did_open.rs b/crates/ty_server/src/server/api/notifications/did_open.rs index c47b285455229..bef5c1cc4a462 100644 --- a/crates/ty_server/src/server/api/notifications/did_open.rs +++ b/crates/ty_server/src/server/api/notifications/did_open.rs @@ -2,7 +2,6 @@ use lsp_types::notification::DidOpenTextDocument; use lsp_types::{DidOpenTextDocumentParams, TextDocumentItem}; use crate::TextDocument; -use crate::document::LanguageId; use crate::server::Result; use crate::server::api::diagnostics::publish_diagnostics_if_needed; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; @@ -31,11 +30,7 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler { }, } = params; - let text_doc = TextDocument::new(uri, text, version).with_language_id(&language_id); - if matches!(text_doc.language_id(), Some(LanguageId::Other)) { - return Ok(()); - } - + let text_doc = TextDocument::new(uri, text, version, &language_id); let document = session.open_text_document(text_doc); publish_diagnostics_if_needed(&document, session, client); diff --git a/crates/ty_server/src/server/api/notifications/did_open_notebook.rs b/crates/ty_server/src/server/api/notifications/did_open_notebook.rs index 854195ad84dcf..d34a60a9425a2 100644 --- a/crates/ty_server/src/server/api/notifications/did_open_notebook.rs +++ b/crates/ty_server/src/server/api/notifications/did_open_notebook.rs @@ -39,9 +39,9 @@ impl SyncNotificationHandler for DidOpenNotebookHandler { let notebook_path = document.notebook_or_file_path(); for cell in params.cell_text_documents { - let cell_document = TextDocument::new(cell.uri, cell.text, cell.version) - .with_language_id(&cell.language_id) - .with_notebook(notebook_path.clone()); + let cell_document = + TextDocument::new(cell.uri, cell.text, cell.version, &cell.language_id) + .with_notebook(notebook_path.clone()); session.open_text_document(cell_document); } diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 5b2bb03fd4e68..e3ee457364670 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -33,7 +33,7 @@ pub(crate) use self::options::InitializationOptions; pub use self::options::{ClientOptions, DiagnosticMode, GlobalOptions, WorkspaceOptions}; pub(crate) use self::settings::{GlobalSettings, WorkspaceSettings}; use crate::capabilities::{ResolvedClientCapabilities, server_diagnostic_options}; -use crate::document::{DocumentKey, DocumentVersion, NotebookDocument}; +use crate::document::{DocumentKey, DocumentVersion, LanguageId, NotebookDocument}; use crate::server::{Action, publish_settings_diagnostics}; use crate::session::client::Client; use crate::session::index::Document; @@ -1166,7 +1166,7 @@ impl Session { /// Returns a handle to the opened document. pub(crate) fn open_notebook_document(&mut self, document: NotebookDocument) -> DocumentHandle { let handle = self.index_mut().open_notebook_document(document); - self.open_document_in_db(&handle); + self.open_document_in_db(&handle, None); handle } @@ -1175,12 +1175,13 @@ impl Session { /// /// Returns a handle to the opened document. pub(crate) fn open_text_document(&mut self, document: TextDocument) -> DocumentHandle { + let language_id = document.language_id(); let handle = self.index_mut().open_text_document(document); - self.open_document_in_db(&handle); + self.open_document_in_db(&handle, Some(language_id)); handle } - fn open_document_in_db(&mut self, document: &DocumentHandle) { + fn open_document_in_db(&mut self, document: &DocumentHandle, language_id: Option) { let path = document.notebook_or_file_path(); // This is a "maybe" because the `File` might've not been interned yet i.e., the @@ -1193,6 +1194,11 @@ impl Session { .is_none_or(|file| !file.exists(db)) }); + // When we know the document isn't a Python source file + // then we'll avoid adding it to the project. (But we + // still track it as part of the index.) + let is_not_python = matches!(language_id, Some(LanguageId::Other)); + match path { AnySystemPath::System(system_path) => { let event = if is_maybe_new_system_file { @@ -1205,6 +1211,10 @@ impl Session { }; self.apply_changes(path, vec![event]); + if is_not_python { + return; + } + let db = self.project_db_mut(path); match system_path_to_file(db, system_path) { Ok(file) => { @@ -1220,6 +1230,10 @@ impl Session { } } AnySystemPath::SystemVirtual(virtual_path) => { + if is_not_python { + return; + } + let db = self.project_db_mut(path); let virtual_file = db.files().virtual_file(db, virtual_path); db.project().open_file(db, virtual_file.file()); diff --git a/crates/ty_server/src/session/index.rs b/crates/ty_server/src/session/index.rs index 95237212cf828..10fc6ddfacc5e 100644 --- a/crates/ty_server/src/session/index.rs +++ b/crates/ty_server/src/session/index.rs @@ -110,10 +110,14 @@ impl Index { self.documents.insert( DocumentKey::from_url(&opened_cell.uri), Document::Text( - TextDocument::new(opened_cell.uri, opened_cell.text, opened_cell.version) - .with_language_id(&opened_cell.language_id) - .with_notebook(notebook_path.clone()) - .into(), + TextDocument::new( + opened_cell.uri, + opened_cell.text, + opened_cell.version, + &opened_cell.language_id, + ) + .with_notebook(notebook_path.clone()) + .into(), ), ); } @@ -231,7 +235,7 @@ impl Document { pub(crate) fn language_id(&self) -> Option { match self { - Self::Text(document) => document.language_id(), + Self::Text(document) => Some(document.language_id()), Self::Notebook(_) => None, } } diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index 709d222ba8fa1..0b6a9c25348e5 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -124,7 +124,7 @@ impl LSPSystem { extension: Option<&str>, ) -> Option { match document { - Document::Text(text) => match text.language_id()? { + Document::Text(text) => match text.language_id() { LanguageId::Python => Some( extension .and_then(PySourceType::try_from_extension) diff --git a/crates/ty_server/tests/e2e/publish_diagnostics.rs b/crates/ty_server/tests/e2e/publish_diagnostics.rs index 951e544145b9f..8801195c274f6 100644 --- a/crates/ty_server/tests/e2e/publish_diagnostics.rs +++ b/crates/ty_server/tests/e2e/publish_diagnostics.rs @@ -429,6 +429,8 @@ def foo() -> str: insta::assert_debug_snapshot!(diagnostics); server.close_text_document(foo); + let diagnostics = server.await_notification::(); + insta::assert_debug_snapshot!(diagnostics); let params = DidOpenTextDocumentParams { text_document: TextDocumentItem { diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension-3.snap b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension-3.snap new file mode 100644 index 0000000000000..91a4a10b81dc0 --- /dev/null +++ b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__changing_language_of_file_without_extension-3.snap @@ -0,0 +1,21 @@ +--- +source: crates/ty_server/tests/e2e/publish_diagnostics.rs +expression: diagnostics +--- +PublishDiagnosticsParams { + uri: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/src/foo", + query: None, + fragment: None, + }, + diagnostics: [], + version: Some( + 1, + ), +} From d7efaf45e37679dfb23f93b68a07f370404ee0e1 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:14:10 -0500 Subject: [PATCH 195/261] Fail CI on new linter ecosystem panics (#23597) Summary -- The huge number of changes in https://github.com/astral-sh/ruff/pull/22205#issuecomment-3696660639 should have obviously been a red flag, but I think it would be nice if CI failed when new ecosystem panics were introduced. This PR adds a check for diagnostic lines that start with `panic: Panicked at crates/`, raises a `ToolError` if any are found in the results from the comparison executable, and then ~~also exits non-zero if any errors are returned~~ fails the CI run if the corresponding error message was printed. After trying this out in CI, I opted not to change the script's exit code itself because that suppressed the ecosystem comment. It feels a little hackier this way but preserves the behavior I wanted of both failing CI and still getting the ecosystem comment to help with debugging. Test Plan -- Local testing on the 0.15.3 tag showing that ruff-ecosystem exited non-zero and some manual testing in CI, as you can see below. --- .github/workflows/ci.yaml | 6 ++++++ python/ruff-ecosystem/ruff_ecosystem/check.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9c662213e2ac6..4dd6abfa8a8ba 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -663,6 +663,12 @@ jobs: path: ecosystem-result if-no-files-found: "error" + - name: Fail on ecosystem errors + run: | + if grep -q "project error" ecosystem-result; then + exit 1 + fi + fuzz-ty: name: "Fuzz for new ty panics" runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }} diff --git a/python/ruff-ecosystem/ruff_ecosystem/check.py b/python/ruff-ecosystem/ruff_ecosystem/check.py index b01faa3ff553e..b1d2af54a8b3b 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/check.py +++ b/python/ruff-ecosystem/ruff_ecosystem/check.py @@ -49,6 +49,8 @@ r"^(?P[+-])? ?(?P.*): (?P[A-Z]{1,4}[0-9]{3,4}|[a-z\-]+:)(?P \[\*\])? (?P.*)" ) +PANIC_DIAGNOSTIC_LINE_RE = re.compile(r"^[^:]+: panic: Panicked at ") + CHECK_VIOLATION_FIX_INDICATOR = " [*]" GITHUB_MAX_COMMENT_LENGTH = 65536 # characters @@ -530,6 +532,10 @@ async def compare_check( comparison_task.result(), ) + for line in comparison_output: + if PANIC_DIAGNOSTIC_LINE_RE.match(line): + raise ToolError(line) + diff = Diff.from_pair(baseline_output, comparison_output) return Comparison(diff=diff, repo=cloned_repo) From a9b2876bd33264c826aaf38e462632f1f7bceb55 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 4 Mar 2026 16:15:04 +0000 Subject: [PATCH 196/261] [ty] Move TypeVar-related code to a `types::typevar` submodule (#23710) --- crates/ty/docs/rules.md | 206 +-- crates/ty_python_semantic/src/types.rs | 1337 +--------------- .../src/types/bound_super.rs | 6 +- .../ty_python_semantic/src/types/call/bind.rs | 13 +- .../src/types/constraints.rs | 4 +- .../src/types/diagnostic.rs | 3 +- .../ty_python_semantic/src/types/display.rs | 9 +- .../ty_python_semantic/src/types/generics.rs | 14 +- .../src/types/infer/builder.rs | 26 +- .../types/infer/builder/binary_expressions.rs | 5 +- .../src/types/known_instance.rs | 3 +- .../ty_python_semantic/src/types/typevar.rs | 1356 +++++++++++++++++ .../ty_python_semantic/src/types/visitor.rs | 8 +- 13 files changed, 1516 insertions(+), 1474 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/typevar.rs diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 5f51db2e505b8..c4672536a4fdc 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -640,7 +640,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -672,7 +672,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -798,7 +798,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -828,7 +828,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -854,7 +854,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -888,7 +888,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -977,7 +977,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1004,7 +1004,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1032,7 +1032,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1066,7 +1066,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1102,7 +1102,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1126,7 +1126,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1153,7 +1153,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1190,7 +1190,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1222,7 +1222,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1251,7 +1251,7 @@ a: str Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1300,7 +1300,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1344,7 +1344,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1386,7 +1386,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1430,7 +1430,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1468,7 +1468,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1547,7 +1547,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1586,7 +1586,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1647,7 +1647,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1682,7 +1682,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1710,7 +1710,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1744,7 +1744,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1851,7 +1851,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1905,7 +1905,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1935,7 +1935,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1985,7 +1985,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2011,7 +2011,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2042,7 +2042,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2076,7 +2076,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2125,7 +2125,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2154,7 +2154,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2250,7 +2250,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2296,7 +2296,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2323,7 +2323,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2370,7 +2370,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2400,7 +2400,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2430,7 +2430,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2464,7 +2464,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2498,7 +2498,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2529,7 +2529,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2576,7 +2576,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2608,7 +2608,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2643,7 +2643,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2674,7 +2674,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2729,7 +2729,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2772,7 +2772,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2797,7 +2797,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2830,7 +2830,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2859,7 +2859,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2885,7 +2885,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2909,7 +2909,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2942,7 +2942,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2975,7 +2975,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3002,7 +3002,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3029,7 +3029,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3057,7 +3057,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3089,7 +3089,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3126,7 +3126,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3190,7 +3190,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3217,7 +3217,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3249,7 +3249,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3283,7 +3283,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3313,7 +3313,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3342,7 +3342,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3376,7 +3376,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3403,7 +3403,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3431,7 +3431,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3477,7 +3477,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -3514,7 +3514,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3538,7 +3538,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3565,7 +3565,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3593,7 +3593,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3651,7 +3651,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3676,7 +3676,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3701,7 +3701,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3740,7 +3740,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3777,7 +3777,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3818,7 +3818,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3846,7 +3846,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: warn · Preview (since 0.0.21) · Related issues · -View source +View source @@ -3952,7 +3952,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4015,7 +4015,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 12d4b2ea5f22c..f3fa233d9eedf 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1,7 +1,7 @@ use compact_str::ToCompactString; use itertools::Itertools; use ruff_diagnostics::{Edit, Fix}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashMap; use std::borrow::Cow; use std::cell::RefCell; @@ -41,8 +41,8 @@ pub(crate) use self::signatures::{CallableSignature, Signature}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; use crate::place::{ - DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening, - builtins_module_scope, imported_symbol, known_module_symbol, + DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, builtins_module_scope, + imported_symbol, known_module_symbol, }; use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::place::ScopedPlaceId; @@ -77,8 +77,11 @@ use crate::types::newtype::NewType; pub(crate) use crate::types::signatures::{Parameter, Parameters}; use crate::types::signatures::{ParameterForm, walk_signature}; use crate::types::special_form::TypeQualifier; -use crate::types::tuple::{Tuple, TupleSpec}; +use crate::types::tuple::TupleSpec; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; +pub use crate::types::typevar::{ + BindingContext, BoundTypeVarInstance, ParamSpecAttrKind, TypeVarBoundOrConstraints, TypeVarKind, +}; pub use crate::types::variance::TypeVarVariance; use crate::types::variance::VarianceInferable; use crate::types::visitor::any_over_type; @@ -129,6 +132,7 @@ mod string_annotation; mod subclass_of; mod tuple; mod typed_dict; +mod typevar; mod unpacker; mod variance; mod visitor; @@ -238,11 +242,6 @@ pub(crate) struct FindLegacyTypeVars; pub(crate) type SpecializationVisitor<'db> = CycleDetector, ()>; pub(crate) struct VisitSpecialization; -/// A [`CycleDetector`] that is used in `TypeVarInstance::default_type`. -pub(crate) type TypeVarDefaultVisitor<'db> = - CycleDetector, Option>>; -pub(crate) struct VisitTypeVarDefault; - /// How a generic type has been specialized. /// /// This matters only if there is at least one invariant type parameter. @@ -1182,49 +1181,10 @@ impl<'db> Type<'db> { ) } - pub(crate) const fn is_type_var(self) -> bool { - matches!(self, Type::TypeVar(_)) - } - - pub(crate) const fn as_typevar(self) -> Option> { - match self { - Type::TypeVar(bound_typevar) => Some(bound_typevar), - _ => None, - } - } - - pub(crate) fn has_typevar(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| matches!(ty, Type::TypeVar(_))) - } - - pub(crate) fn has_non_self_typevar(self, db: &'db dyn Db) -> bool { - any_over_type( - db, - self, - false, - |ty| matches!(ty, Type::TypeVar(tv) if !tv.typevar(db).is_self(db)), - ) - } - - pub(crate) fn has_unspecialized_type_var(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| { - matches!(ty, Type::Dynamic(DynamicType::UnspecializedTypeVar)) - }) - } - pub(crate) fn has_dynamic(self, db: &'db dyn Db) -> bool { any_over_type(db, self, false, |ty| ty.is_dynamic()) } - pub(crate) fn has_typevar_or_typevar_instance(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| { - matches!( - ty, - Type::KnownInstance(KnownInstanceType::TypeVar(_)) | Type::TypeVar(_) - ) - }) - } - pub(crate) const fn as_special_form(self) -> Option { match self { Type::SpecialForm(special_form) => Some(special_form), @@ -6733,1287 +6693,6 @@ impl<'db> InvalidTypeExpression<'db> { } } -/// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax, -/// or an implicit typevar like `Self` was used. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] -pub enum TypeVarKind { - /// `T = TypeVar("T")` - Legacy, - /// `def foo[T](x: T) -> T: ...` - Pep695, - /// `typing.Self` - TypingSelf, - /// `P = ParamSpec("P")` - ParamSpec, - /// `def foo[**P]() -> None: ...` - Pep695ParamSpec, - /// `Alias: typing.TypeAlias = T` - Pep613Alias, -} - -impl TypeVarKind { - const fn is_self(self) -> bool { - matches!(self, Self::TypingSelf) - } - - const fn is_paramspec(self) -> bool { - matches!(self, Self::ParamSpec | Self::Pep695ParamSpec) - } -} - -/// The identity of a type variable. -/// -/// This represents the core identity of a typevar, independent of its bounds or constraints. Two -/// typevars have the same identity if they represent the same logical typevar, even if their -/// bounds have been materialized differently. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct TypeVarIdentity<'db> { - /// The name of this TypeVar (e.g. `T`) - #[returns(ref)] - pub(crate) name: ast::name::Name, - - /// The type var's definition (None if synthesized) - pub(crate) definition: Option>, - - /// The kind of typevar (PEP 695, Legacy, or TypingSelf) - pub(crate) kind: TypeVarKind, -} - -impl get_size2::GetSize for TypeVarIdentity<'_> {} - -impl<'db> TypeVarIdentity<'db> { - fn with_name_suffix(self, db: &'db dyn Db, suffix: &str) -> Self { - let name = format!("{}'{}", self.name(db), suffix); - Self::new( - db, - ast::name::Name::from(name), - self.definition(db), - self.kind(db), - ) - } -} - -/// A specific instance of a type variable that has not been bound to a generic context yet. -/// -/// This is usually not the type that you want; if you are working with a typevar, in a generic -/// context, which might be specialized to a concrete type, you want [`BoundTypeVarInstance`]. This -/// type holds information that does not depend on which generic context the typevar is used in. -/// -/// For a legacy typevar: -/// -/// ```py -/// T = TypeVar("T") # [1] -/// def generic_function(t: T) -> T: ... # [2] -/// ``` -/// -/// we will create a `TypeVarInstance` for the typevar `T` when it is instantiated. The type of `T` -/// at `[1]` will be a `KnownInstanceType::TypeVar` wrapping this `TypeVarInstance`. The typevar is -/// not yet bound to any generic context at this point. -/// -/// The typevar is used in `generic_function`, which binds it to a new generic context. We will -/// create a [`BoundTypeVarInstance`] for this new binding of the typevar. The type of `T` at `[2]` -/// will be a `Type::TypeVar` wrapping this `BoundTypeVarInstance`. -/// -/// For a PEP 695 typevar: -/// -/// ```py -/// def generic_function[T](t: T) -> T: ... -/// # ╰─────╰─────────── [2] -/// # ╰─────────────────────── [1] -/// ``` -/// -/// the typevar is defined and immediately bound to a single generic context. Just like in the -/// legacy case, we will create a `TypeVarInstance` and [`BoundTypeVarInstance`], and the type of -/// `T` at `[1]` and `[2]` will be that `TypeVarInstance` and `BoundTypeVarInstance`, respectively. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct TypeVarInstance<'db> { - /// The identity of this typevar - pub(crate) identity: TypeVarIdentity<'db>, - - /// The upper bound or constraint on the type of this TypeVar, if any. Don't use this field - /// directly; use the `bound_or_constraints` (or `upper_bound` and `constraints`) methods - /// instead (to evaluate any lazy bound or constraints). - _bound_or_constraints: Option>, - - /// The explicitly specified variance of the TypeVar - explicit_variance: Option, - - /// The default type for this TypeVar, if any. Don't use this field directly, use the - /// `default_type` method instead (to evaluate any lazy default). - _default: Option>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for TypeVarInstance<'_> {} - -fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - typevar: TypeVarInstance<'db>, - visitor: &V, -) { - if let Some(bound_or_constraints) = if visitor.should_visit_lazy_type_attributes() { - typevar.bound_or_constraints(db) - } else { - match typevar._bound_or_constraints(db) { - _ if visitor.should_visit_lazy_type_attributes() => typevar.bound_or_constraints(db), - Some(TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints)) => { - Some(bound_or_constraints) - } - _ => None, - } - } { - walk_type_var_bounds(db, bound_or_constraints, visitor); - } - if let Some(default_type) = if visitor.should_visit_lazy_type_attributes() { - typevar.default_type(db) - } else { - match typevar._default(db) { - Some(TypeVarDefaultEvaluation::Eager(default_type)) => Some(default_type), - _ => None, - } - } { - visitor.visit_type(db, default_type); - } -} - -#[salsa::tracked] -impl<'db> TypeVarInstance<'db> { - pub(crate) fn with_binding_context( - self, - db: &'db dyn Db, - binding_context: Definition<'db>, - ) -> BoundTypeVarInstance<'db> { - BoundTypeVarInstance::new(db, self, BindingContext::Definition(binding_context), None) - } - - fn with_name_suffix(self, db: &'db dyn Db, suffix: &str) -> Self { - Self::new( - db, - self.identity(db).with_name_suffix(db, suffix), - self._bound_or_constraints(db), - self.explicit_variance(db), - self._default(db), - ) - } - - pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { - self.identity(db).name(db) - } - - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { - self.identity(db).definition(db) - } - - pub fn kind(self, db: &'db dyn Db) -> TypeVarKind { - self.identity(db).kind(db) - } - - pub(crate) fn is_self(self, db: &'db dyn Db) -> bool { - matches!(self.kind(db), TypeVarKind::TypingSelf) - } - - pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { - self.kind(db).is_paramspec() - } - - pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { - if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { - Some(ty) - } else { - None - } - } - - pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&'db [Type<'db>]> { - if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { - Some(tuple.elements(db)) - } else { - None - } - } - - pub(crate) fn bound_or_constraints( - self, - db: &'db dyn Db, - ) -> Option> { - self._bound_or_constraints(db).and_then(|w| match w { - TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { - Some(bound_or_constraints) - } - TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self - .lazy_bound(db) - .map(TypeVarBoundOrConstraints::UpperBound), - TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self - .lazy_constraints(db) - .map(TypeVarBoundOrConstraints::Constraints), - }) - } - - /// Returns the bounds or constraints of this typevar. If the typevar is unbounded, returns - /// `object` as its upper bound. - pub(crate) fn require_bound_or_constraints( - self, - db: &'db dyn Db, - ) -> TypeVarBoundOrConstraints<'db> { - self.bound_or_constraints(db) - .unwrap_or_else(|| TypeVarBoundOrConstraints::UpperBound(Type::object())) - } - - pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { - let visitor = TypeVarDefaultVisitor::new(None); - self.default_type_impl(db, &visitor) - } - - fn default_type_impl( - self, - db: &'db dyn Db, - visitor: &TypeVarDefaultVisitor<'db>, - ) -> Option> { - visitor.visit(self, || { - self._default(db).and_then(|default| match default { - TypeVarDefaultEvaluation::Eager(ty) => Some(ty), - TypeVarDefaultEvaluation::Lazy => self.lazy_default_impl(db, visitor), - }) - }) - } - - fn materialize_impl( - self, - db: &'db dyn Db, - materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, - ) -> Self { - Self::new( - db, - self.identity(db), - self._bound_or_constraints(db) - .and_then(|bound_or_constraints| match bound_or_constraints { - TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => Some( - bound_or_constraints - .materialize_impl(db, materialization_kind, visitor) - .into(), - ), - TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => { - self.lazy_bound(db).map(|bound| { - TypeVarBoundOrConstraints::UpperBound(bound) - .materialize_impl(db, materialization_kind, visitor) - .into() - }) - } - TypeVarBoundOrConstraintsEvaluation::LazyConstraints => { - self.lazy_constraints(db).map(|constraints| { - TypeVarBoundOrConstraints::Constraints(constraints) - .materialize_impl(db, materialization_kind, visitor) - .into() - }) - } - }), - self.explicit_variance(db), - self._default(db).and_then(|default| match default { - TypeVarDefaultEvaluation::Eager(ty) => { - Some(ty.materialize(db, materialization_kind, visitor).into()) - } - TypeVarDefaultEvaluation::Lazy => self - .lazy_default(db) - .map(|ty| ty.materialize(db, materialization_kind, visitor).into()), - }), - ) - } - - fn to_instance(self, db: &'db dyn Db) -> Option { - let bound_or_constraints = match self.bound_or_constraints(db)? { - TypeVarBoundOrConstraints::UpperBound(upper_bound) => { - TypeVarBoundOrConstraints::UpperBound(upper_bound.to_instance(db)?) - } - TypeVarBoundOrConstraints::Constraints(constraints) => { - TypeVarBoundOrConstraints::Constraints(constraints.to_instance(db)?) - } - }; - let identity = TypeVarIdentity::new( - db, - Name::new(format!("{}'instance", self.name(db))), - None, // definition - self.kind(db), - ); - Some(Self::new( - db, - identity, - Some(bound_or_constraints.into()), - self.explicit_variance(db), - None, // _default - )) - } - - fn type_is_self_referential( - self, - db: &'db dyn Db, - ty: Type<'db>, - visitor: &TypeVarDefaultVisitor<'db>, - ) -> bool { - #[derive(Copy, Clone)] - struct State<'db, 'a> { - db: &'db dyn Db, - visitor: &'a TypeVarDefaultVisitor<'db>, - seen_typevars: &'a RefCell>>, - seen_type_aliases: &'a RefCell>>, - } - - fn typevar_default_is_self_referential<'db>( - state: State<'db, '_>, - typevar: TypeVarInstance<'db>, - self_identity: TypeVarIdentity<'db>, - ) -> bool { - if typevar.identity(state.db) == self_identity { - return true; - } - - if !state.seen_typevars.borrow_mut().insert(typevar) { - return false; - } - - typevar - .default_type_impl(state.db, state.visitor) - .is_some_and(|default_ty| { - type_is_self_referential_impl(state, default_ty, self_identity) - }) - } - - fn type_alias_is_self_referential<'db>( - state: State<'db, '_>, - type_alias: TypeAliasType<'db>, - self_identity: TypeVarIdentity<'db>, - ) -> bool { - if !state.seen_type_aliases.borrow_mut().insert(type_alias) { - return false; - } - - type_is_self_referential_impl(state, type_alias.raw_value_type(state.db), self_identity) - } - - fn type_is_self_referential_impl<'db>( - state: State<'db, '_>, - ty: Type<'db>, - self_identity: TypeVarIdentity<'db>, - ) -> bool { - any_over_type(state.db, ty, false, |inner_ty| match inner_ty { - Type::TypeVar(bound_typevar) => typevar_default_is_self_referential( - state, - bound_typevar.typevar(state.db), - self_identity, - ), - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { - typevar_default_is_self_referential(state, typevar, self_identity) - } - Type::TypeAlias(alias) => { - type_alias_is_self_referential(state, alias, self_identity) - } - Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => { - type_alias_is_self_referential(state, alias, self_identity) - } - _ => false, - }) - } - - let seen_typevars = RefCell::new(FxHashSet::default()); - let seen_type_aliases = RefCell::new(FxHashSet::default()); - - let state = State { - db, - visitor, - seen_typevars: &seen_typevars, - seen_type_aliases: &seen_type_aliases, - }; - - type_is_self_referential_impl(state, ty, self.identity(db)) - } - - /// Returns the "unchecked" upper bound of a type variable instance. - /// `lazy_bound` checks if the upper bound type is generic (generic upper bound is not allowed). - #[salsa::tracked( - cycle_fn=lazy_bound_cycle_recover, - cycle_initial=|_, _, _| None, - heap_size=ruff_memory_usage::heap_size - )] - fn lazy_bound_unchecked(self, db: &'db dyn Db) -> Option> { - let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); - let ty = match definition.kind(db) { - // PEP 695 typevar - DefinitionKind::TypeVar(typevar) => { - let typevar_node = typevar.node(&module); - definition_expression_type(db, definition, typevar_node.bound.as_ref()?) - } - // legacy typevar - DefinitionKind::Assignment(assignment) => { - let call_expr = assignment.value(&module).as_call_expr()?; - let expr = &call_expr.arguments.find_keyword("bound")?.value; - definition_expression_type(db, definition, expr) - } - _ => return None, - }; - - Some(ty) - } - - fn lazy_bound(self, db: &'db dyn Db) -> Option> { - let bound = self.lazy_bound_unchecked(db)?; - - if bound.has_typevar_or_typevar_instance(db) { - return None; - } - - Some(bound) - } - - /// Returns the "unchecked" constraints of a type variable instance. - /// `lazy_constraints` checks if any of the constraint types are generic (generic constraints are not allowed). - #[salsa::tracked( - cycle_fn=lazy_constraints_cycle_recover, - cycle_initial=|_, _, _| None, - heap_size=ruff_memory_usage::heap_size - )] - fn lazy_constraints_unchecked(self, db: &'db dyn Db) -> Option> { - let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); - let constraints = match definition.kind(db) { - // PEP 695 typevar - DefinitionKind::TypeVar(typevar) => { - let typevar_node = typevar.node(&module); - let bound = - definition_expression_type(db, definition, typevar_node.bound.as_ref()?); - let constraints = if let Some(tuple) = bound.tuple_instance_spec(db) - && let Tuple::Fixed(tuple) = tuple.into_owned() - { - tuple.owned_elements() - } else { - vec![Type::unknown()].into_boxed_slice() - }; - TypeVarConstraints::new(db, constraints) - } - // legacy typevar - DefinitionKind::Assignment(assignment) => { - let call_expr = assignment.value(&module).as_call_expr()?; - TypeVarConstraints::new( - db, - call_expr - .arguments - .args - .iter() - .skip(1) - .map(|arg| definition_expression_type(db, definition, arg)) - .collect::>(), - ) - } - _ => return None, - }; - - Some(constraints) - } - - fn lazy_constraints(self, db: &'db dyn Db) -> Option> { - let constraints = self.lazy_constraints_unchecked(db)?; - - if constraints - .elements(db) - .iter() - .any(|ty| ty.has_typevar_or_typevar_instance(db)) - { - return None; - } - - Some(constraints) - } - - /// Returns the "unchecked" default type of a type variable instance. - /// `lazy_default` checks if the default type is not self-referential. - #[salsa::tracked(cycle_initial=|_, id, _| Some(Type::divergent(id)), cycle_fn=lazy_default_cycle_recover, heap_size=ruff_memory_usage::heap_size)] - fn lazy_default_unchecked(self, db: &'db dyn Db) -> Option> { - fn convert_type_to_paramspec_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - let parameters = match ty { - Type::NominalInstance(nominal_instance) - if nominal_instance.has_known_class(db, KnownClass::EllipsisType) => - { - Parameters::gradual_form() - } - Type::NominalInstance(nominal_instance) => nominal_instance - .own_tuple_spec(db) - .map_or_else(Parameters::unknown, |tuple_spec| { - Parameters::new( - db, - tuple_spec - .iter_all_elements() - .map(|ty| Parameter::positional_only(None).with_annotated_type(ty)), - ) - }), - Type::Dynamic(dynamic) => match dynamic { - DynamicType::Todo(_) - | DynamicType::TodoUnpack - | DynamicType::TodoStarredExpression - | DynamicType::TodoFunctionalTypedDict - | DynamicType::TodoTypeVarTuple => Parameters::todo(), - DynamicType::Any - | DynamicType::Unknown - | DynamicType::UnknownGeneric(_) - | DynamicType::UnspecializedTypeVar - | DynamicType::Divergent(_) => Parameters::unknown(), - }, - Type::TypeVar(typevar) if typevar.is_paramspec(db) => { - return ty; - } - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) - if typevar.is_paramspec(db) => - { - return ty; - } - _ => Parameters::unknown(), - }; - Type::paramspec_value_callable(db, parameters) - } - - let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); - let ty = match definition.kind(db) { - // PEP 695 typevar - DefinitionKind::TypeVar(typevar) => { - let typevar_node = typevar.node(&module); - definition_expression_type(db, definition, typevar_node.default.as_ref()?) - } - // legacy typevar / ParamSpec - DefinitionKind::Assignment(assignment) => { - let call_expr = assignment.value(&module).as_call_expr()?; - let func_ty = definition_expression_type(db, definition, &call_expr.func); - let known_class = func_ty.as_class_literal().and_then(|cls| cls.known(db)); - let expr = &call_expr.arguments.find_keyword("default")?.value; - let default_type = definition_expression_type(db, definition, expr); - if known_class == Some(KnownClass::ParamSpec) { - convert_type_to_paramspec_value(db, default_type) - } else { - default_type - } - } - // PEP 695 ParamSpec - DefinitionKind::ParamSpec(paramspec) => { - let paramspec_node = paramspec.node(&module); - let default_ty = - definition_expression_type(db, definition, paramspec_node.default.as_ref()?); - convert_type_to_paramspec_value(db, default_ty) - } - _ => return None, - }; - - Some(ty) - } - - fn lazy_default(self, db: &'db dyn Db) -> Option> { - let visitor = TypeVarDefaultVisitor::new(None); - self.lazy_default_impl(db, &visitor) - } - - fn lazy_default_impl( - self, - db: &'db dyn Db, - visitor: &TypeVarDefaultVisitor<'db>, - ) -> Option> { - let default = self.lazy_default_unchecked(db)?; - - // Unlike bounds/constraints, default types are allowed to be generic (https://peps.python.org/pep-0696/#using-another-type-parameter-as-default). - // Here we simply check for non-self-referential. - // TODO: We should also check for non-forward references. - if self.type_is_self_referential(db, default, visitor) { - return None; - } - - Some(default) - } - - pub fn bind_pep695(self, db: &'db dyn Db) -> Option> { - if !matches!( - self.identity(db).kind(db), - TypeVarKind::Pep695 | TypeVarKind::Pep695ParamSpec - ) { - return None; - } - let typevar_definition = self.definition(db)?; - let index = semantic_index(db, typevar_definition.file(db)); - let (_, child) = index - .child_scopes(typevar_definition.file_scope(db)) - .next()?; - child - .node() - .generic_context(db, index)? - .binds_typevar(db, self) - } -} - -#[expect(clippy::ref_option)] -fn lazy_bound_cycle_recover<'db>( - db: &'db dyn Db, - cycle: &salsa::Cycle, - previous: &Option>, - current: Option>, - _typevar: TypeVarInstance<'db>, -) -> Option> { - // Normalize the bounds/constraints to ensure cycle convergence. - match (previous, current) { - (Some(prev), Some(current)) => Some(current.cycle_normalized(db, *prev, cycle)), - (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), - (_, None) => None, - } -} - -#[allow(clippy::trivially_copy_pass_by_ref)] -#[expect(clippy::ref_option)] -fn lazy_constraints_cycle_recover<'db>( - db: &'db dyn Db, - cycle: &salsa::Cycle, - previous: &Option>, - current: Option>, - _typevar: TypeVarInstance<'db>, -) -> Option> { - // Normalize the bounds/constraints to ensure cycle convergence. - match (previous, current) { - (Some(prev), Some(constraints)) => Some(constraints.cycle_normalized(db, *prev, cycle)), - (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), - (_, None) => None, - } -} - -#[expect(clippy::ref_option)] -fn lazy_default_cycle_recover<'db>( - db: &'db dyn Db, - cycle: &salsa::Cycle, - previous_default: &Option>, - default: Option>, - _typevar: TypeVarInstance<'db>, -) -> Option> { - // Normalize the default to ensure cycle convergence. - match (previous_default, default) { - (Some(prev), Some(default)) => Some(default.cycle_normalized(db, *prev, cycle)), - (None, Some(default)) => Some(default.recursive_type_normalized(db, cycle)), - (_, None) => None, - } -} - -/// Where a type variable is bound and usable. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] -pub enum BindingContext<'db> { - /// The definition of the generic class, function, or type alias that binds this typevar. - Definition(Definition<'db>), - /// The typevar is synthesized internally, and is not associated with a particular definition - /// in the source, but is still bound and eligible for specialization inference. - Synthetic, -} - -impl<'db> From> for BindingContext<'db> { - fn from(definition: Definition<'db>) -> Self { - BindingContext::Definition(definition) - } -} - -impl<'db> BindingContext<'db> { - pub(crate) fn definition(self) -> Option> { - match self { - BindingContext::Definition(definition) => Some(definition), - BindingContext::Synthetic => None, - } - } - - fn name(self, db: &'db dyn Db) -> Option { - self.definition().and_then(|definition| definition.name(db)) - } -} - -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, get_size2::GetSize)] -pub enum ParamSpecAttrKind { - Args, - Kwargs, -} - -impl std::fmt::Display for ParamSpecAttrKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ParamSpecAttrKind::Args => f.write_str("args"), - ParamSpecAttrKind::Kwargs => f.write_str("kwargs"), - } - } -} - -/// The identity of a bound type variable. -/// -/// This identifies a specific binding of a typevar to a context (e.g., `T@ClassC` vs `T@FunctionF`), -/// independent of the typevar's bounds or constraints. Two bound typevars have the same identity -/// if they represent the same logical typevar bound in the same context, even if their bounds -/// have been materialized differently. -#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] -pub struct BoundTypeVarIdentity<'db> { - pub(crate) identity: TypeVarIdentity<'db>, - pub(crate) binding_context: BindingContext<'db>, - /// If [`Some`], this indicates that this type variable is the `args` or `kwargs` component - /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. - paramspec_attr: Option, -} - -/// A type variable that has been bound to a generic context, and which can be specialized to a -/// concrete type. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct BoundTypeVarInstance<'db> { - pub typevar: TypeVarInstance<'db>, - binding_context: BindingContext<'db>, - /// If [`Some`], this indicates that this type variable is the `args` or `kwargs` component - /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. - paramspec_attr: Option, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for BoundTypeVarInstance<'_> {} - -impl<'db> BoundTypeVarInstance<'db> { - pub(crate) fn with_name_suffix(self, db: &'db dyn Db, suffix: &str) -> Self { - Self::new( - db, - self.typevar(db).with_name_suffix(db, suffix), - self.binding_context(db), - self.paramspec_attr(db), - ) - } - - /// Get the identity of this bound typevar. - /// - /// This is used for comparing whether two bound typevars represent the same logical typevar, - /// regardless of e.g. differences in their bounds or constraints due to materialization. - pub(crate) fn identity(self, db: &'db dyn Db) -> BoundTypeVarIdentity<'db> { - BoundTypeVarIdentity { - identity: self.typevar(db).identity(db), - binding_context: self.binding_context(db), - paramspec_attr: self.paramspec_attr(db), - } - } - - pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { - self.typevar(db).name(db) - } - - pub(crate) fn kind(self, db: &'db dyn Db) -> TypeVarKind { - self.typevar(db).kind(db) - } - - pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { - self.kind(db).is_paramspec() - } - - /// Returns a new bound typevar instance with the given `ParamSpec` attribute set. - /// - /// This method will also set an appropriate upper bound on the typevar, based on the - /// attribute kind. For `P.args`, the upper bound will be `tuple[object, ...]`, and for - /// `P.kwargs`, the upper bound will be `Top[dict[str, Any]]`. - /// - /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` - /// type variable. - pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { - debug_assert!( - self.is_paramspec(db), - "Expected a ParamSpec, got {:?}", - self.kind(db) - ); - - let upper_bound = TypeVarBoundOrConstraints::UpperBound(match kind { - ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, Type::object()), - ParamSpecAttrKind::Kwargs => KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]) - .top_materialization(db), - }); - - let typevar = TypeVarInstance::new( - db, - self.typevar(db).identity(db), - Some(TypeVarBoundOrConstraintsEvaluation::Eager(upper_bound)), - None, // ParamSpecs cannot have explicit variance - None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can - ); - - Self::new(db, typevar, self.binding_context(db), Some(kind)) - } - - /// Returns a new bound typevar instance without any `ParamSpec` attribute set. - /// - /// This method will also remove any upper bound that was set by `with_paramspec_attr`. This - /// means that the returned typevar will have no upper bound or constraints. - /// - /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` - /// type variable. - pub(crate) fn without_paramspec_attr(self, db: &'db dyn Db) -> Self { - debug_assert!( - self.is_paramspec(db), - "Expected a ParamSpec, got {:?}", - self.kind(db) - ); - - Self::new( - db, - TypeVarInstance::new( - db, - self.typevar(db).identity(db), - None, // Remove the upper bound set by `with_paramspec_attr` - None, // ParamSpecs cannot have explicit variance - None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can - ), - self.binding_context(db), - None, - ) - } - - /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g. - /// differences in their bounds or constraints due to materialization. - pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { - self.identity(db) == other.identity(db) - } - - /// Create a new PEP 695 type variable that can be used in signatures - /// of synthetic generic functions. - pub(crate) fn synthetic(db: &'db dyn Db, name: Name, variance: TypeVarVariance) -> Self { - let identity = TypeVarIdentity::new( - db, - name, - None, // definition - TypeVarKind::Pep695, - ); - let typevar = TypeVarInstance::new( - db, - identity, - None, // _bound_or_constraints - Some(variance), - None, // _default - ); - Self::new(db, typevar, BindingContext::Synthetic, None) - } - - /// Create a new synthetic `Self` type variable with the given upper bound. - pub(crate) fn synthetic_self( - db: &'db dyn Db, - upper_bound: Type<'db>, - binding_context: BindingContext<'db>, - ) -> Self { - let identity = TypeVarIdentity::new( - db, - Name::new_static("Self"), - None, // definition - TypeVarKind::TypingSelf, - ); - let typevar = TypeVarInstance::new( - db, - identity, - Some(TypeVarBoundOrConstraints::UpperBound(upper_bound).into()), - Some(TypeVarVariance::Invariant), - None, // _default - ); - Self::new(db, typevar, binding_context, None) - } - - /// Returns an identical type variable with its `TypeVarBoundOrConstraints` mapped by the - /// provided closure. - pub(crate) fn map_bound_or_constraints( - self, - db: &'db dyn Db, - f: impl FnOnce(Option>) -> Option>, - ) -> Self { - let bound_or_constraints = f(self.typevar(db).bound_or_constraints(db)); - let typevar = TypeVarInstance::new( - db, - self.typevar(db).identity(db), - bound_or_constraints.map(TypeVarBoundOrConstraintsEvaluation::Eager), - self.typevar(db).explicit_variance(db), - self.typevar(db)._default(db), - ); - - Self::new( - db, - typevar, - self.binding_context(db), - self.paramspec_attr(db), - ) - } - - pub(crate) fn variance_with_polarity( - self, - db: &'db dyn Db, - polarity: TypeVarVariance, - ) -> TypeVarVariance { - let _span = tracing::trace_span!("variance_with_polarity").entered(); - match self.typevar(db).explicit_variance(db) { - Some(explicit_variance) => explicit_variance.compose(polarity), - None => match self.binding_context(db) { - BindingContext::Definition(definition) => binding_type(db, definition) - .with_polarity(polarity) - .variance_of(db, self), - BindingContext::Synthetic => TypeVarVariance::Invariant, - }, - } - } - - pub fn variance(self, db: &'db dyn Db) -> TypeVarVariance { - self.variance_with_polarity(db, TypeVarVariance::Covariant) - } - - fn apply_type_mapping_impl<'a>( - self, - db: &'db dyn Db, - type_mapping: &TypeMapping<'a, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, - ) -> Type<'db> { - match type_mapping { - TypeMapping::ApplySpecialization(specialization) => { - let typevar = if self.is_paramspec(db) { - self.without_paramspec_attr(db) - } else { - self - }; - specialization - .get(db, typevar) - .map(|ty| { - if let Some(attr) = self.paramspec_attr(db) - && let Type::TypeVar(typevar) = ty - && typevar.is_paramspec(db) - { - return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); - } - ty - }) - .unwrap_or(Type::TypeVar(self)) - } - TypeMapping::BindSelf(binding) => { - if binding.should_bind(db, self) { - binding.self_type() - } else { - Type::TypeVar(self) - } - } - TypeMapping::ReplaceSelf { new_upper_bound } => { - if self.typevar(db).is_self(db) { - Type::TypeVar(BoundTypeVarInstance::synthetic_self( - db, - *new_upper_bound, - self.binding_context(db), - )) - } else { - Type::TypeVar(self) - } - } - TypeMapping::UniqueSpecialization { .. } - | TypeMapping::PromoteLiterals(_) - | TypeMapping::ReplaceParameterDefaults - | TypeMapping::BindLegacyTypevars(_) - | TypeMapping::EagerExpansion - | TypeMapping::RescopeReturnCallables(_) => Type::TypeVar(self), - TypeMapping::Materialize(materialization_kind) => { - Type::TypeVar(self.materialize_impl(db, *materialization_kind, visitor)) - } - } - } -} - -fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - bound_typevar: BoundTypeVarInstance<'db>, - visitor: &V, -) { - visitor.visit_type_var_type(db, bound_typevar.typevar(db)); -} - -impl<'db> BoundTypeVarInstance<'db> { - /// Returns the default value of this typevar, recursively applying its binding context to any - /// other typevars that appear in the default. - /// - /// For instance, in - /// - /// ```py - /// T = TypeVar("T") - /// U = TypeVar("U", default=T) - /// - /// # revealed: typing.TypeVar[U = typing.TypeVar[T]] - /// reveal_type(U) - /// - /// # revealed: typing.Generic[T, U = T@C] - /// class C(reveal_type(Generic[T, U])): ... - /// ``` - /// - /// In the first case, the use of `U` is unbound, and so we have a [`TypeVarInstance`], and its - /// default value (`T`) is also unbound. - /// - /// By using `U` in the generic class, it becomes bound, and so we have a - /// `BoundTypeVarInstance`. As part of binding `U` we must also bind its default value - /// (resulting in `T@C`). - pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { - bound_typevar_default_type(db, self) - } - - fn materialize_impl( - self, - db: &'db dyn Db, - materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, - ) -> Self { - Self::new( - db, - self.typevar(db) - .materialize_impl(db, materialization_kind, visitor), - self.binding_context(db), - self.paramspec_attr(db), - ) - } - - fn to_instance(self, db: &'db dyn Db) -> Option { - Some(Self::new( - db, - self.typevar(db).to_instance(db)?, - self.binding_context(db), - self.paramspec_attr(db), - )) - } -} - -#[salsa::tracked( - cycle_initial=|_, _, _| None, - cycle_fn=bound_typevar_default_type_cycle_recover, - heap_size=ruff_memory_usage::heap_size -)] -fn bound_typevar_default_type<'db>( - db: &'db dyn Db, - bound_typevar: BoundTypeVarInstance<'db>, -) -> Option> { - let binding_context = bound_typevar.binding_context(db); - bound_typevar.typevar(db).default_type(db).map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::BindLegacyTypevars(binding_context), - TypeContext::default(), - ) - }) -} - -#[expect(clippy::ref_option)] -fn bound_typevar_default_type_cycle_recover<'db>( - _db: &'db dyn Db, - _cycle: &salsa::Cycle, - _previous_default: &Option>, - _default: Option>, - _bound_typevar: BoundTypeVarInstance<'db>, -) -> Option> { - None -} - -/// Whether a typevar default is eagerly specified or lazily evaluated. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub enum TypeVarDefaultEvaluation<'db> { - /// The default type is lazily evaluated. - Lazy, - /// The default type is eagerly specified. - Eager(Type<'db>), -} - -impl<'db> From> for TypeVarDefaultEvaluation<'db> { - fn from(value: Type<'db>) -> Self { - TypeVarDefaultEvaluation::Eager(value) - } -} - -/// Whether a typevar bound/constraints is eagerly specified or lazily evaluated. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub enum TypeVarBoundOrConstraintsEvaluation<'db> { - /// There is a lazily-evaluated upper bound. - LazyUpperBound, - /// There is a lazily-evaluated set of constraints. - LazyConstraints, - /// The upper bound/constraints are eagerly specified. - Eager(TypeVarBoundOrConstraints<'db>), -} - -impl<'db> From> for TypeVarBoundOrConstraintsEvaluation<'db> { - fn from(value: TypeVarBoundOrConstraints<'db>) -> Self { - TypeVarBoundOrConstraintsEvaluation::Eager(value) - } -} - -/// Type variable constraints (e.g. `T: (int, str)`). -/// This is structurally identical to [`UnionType`], except that it does not perform simplification and preserves the element types. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct TypeVarConstraints<'db> { - #[returns(ref)] - elements: Box<[Type<'db>]>, -} - -impl get_size2::GetSize for TypeVarConstraints<'_> {} - -fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - constraints: TypeVarConstraints<'db>, - visitor: &V, -) { - for ty in constraints.elements(db) { - visitor.visit_type(db, *ty); - } -} - -impl<'db> TypeVarConstraints<'db> { - fn as_type(self, db: &'db dyn Db) -> Type<'db> { - UnionType::from_elements(db, self.elements(db)) - } - - fn to_instance(self, db: &'db dyn Db) -> Option> { - let mut instance_elements = Vec::new(); - for ty in self.elements(db) { - instance_elements.push(ty.to_instance(db)?); - } - Some(TypeVarConstraints::new( - db, - instance_elements.into_boxed_slice(), - )) - } - - fn map(self, db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Type<'db>) -> Self { - let mapped = self - .elements(db) - .iter() - .map(transform_fn) - .collect::>(); - TypeVarConstraints::new(db, mapped) - } - - pub(crate) fn map_with_boundness_and_qualifiers( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, - ) -> PlaceAndQualifiers<'db> { - let mut builder = UnionBuilder::new(db); - let mut qualifiers = TypeQualifiers::empty(); - - let mut all_unbound = true; - let mut possibly_unbound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.elements(db) { - let PlaceAndQualifiers { - place: ty_member, - qualifiers: new_qualifiers, - } = transform_fn(ty); - qualifiers |= new_qualifiers; - match ty_member { - Place::Undefined => { - possibly_unbound = true; - } - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - if member_boundness == Definedness::PossiblyUndefined { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - PlaceAndQualifiers { - place: if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder.build(), - origin, - definedness: if possibly_unbound { - Definedness::PossiblyUndefined - } else { - Definedness::AlwaysDefined - }, - widening: Widening::None, - }) - }, - qualifiers, - } - } - - fn materialize_impl( - self, - db: &'db dyn Db, - materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, - ) -> Self { - let materialized = self - .elements(db) - .iter() - .map(|ty| ty.materialize(db, materialization_kind, visitor)) - .collect::>(); - TypeVarConstraints::new(db, materialized) - } - - /// Normalize for cycle recovery by combining with the previous value and - /// removing divergent types introduced by the cycle. - /// - /// See [`Type::cycle_normalized`] for more details on how this works. - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { - let current_elements = self.elements(db); - let prev_elements = previous.elements(db); - TypeVarConstraints::new( - db, - current_elements - .iter() - .zip(prev_elements.iter()) - .map(|(ty, prev_ty)| ty.cycle_normalized(db, *prev_ty, cycle)) - .collect::>(), - ) - } - - /// Normalize recursive types for cycle recovery when there's no previous value. - /// - /// See [`Type::recursive_type_normalized`] for more details. - fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { - self.map(db, |ty| ty.recursive_type_normalized(db, cycle)) - } -} - -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub enum TypeVarBoundOrConstraints<'db> { - UpperBound(Type<'db>), - Constraints(TypeVarConstraints<'db>), -} - -fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - bounds: TypeVarBoundOrConstraints<'db>, - visitor: &V, -) { - match bounds { - TypeVarBoundOrConstraints::UpperBound(bound) => visitor.visit_type(db, bound), - TypeVarBoundOrConstraints::Constraints(constraints) => { - walk_type_var_constraints(db, constraints, visitor); - } - } -} - -impl<'db> TypeVarBoundOrConstraints<'db> { - fn materialize_impl( - self, - db: &'db dyn Db, - materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, - ) -> Self { - match self { - TypeVarBoundOrConstraints::UpperBound(bound) => TypeVarBoundOrConstraints::UpperBound( - bound.materialize(db, materialization_kind, visitor), - ), - TypeVarBoundOrConstraints::Constraints(constraints) => { - TypeVarBoundOrConstraints::Constraints(constraints.materialize_impl( - db, - materialization_kind, - visitor, - )) - } - } - } -} - /// Whether a given type originates from value expression inference or type expression inference. /// For example, the symbol `int` would be inferred as `` in value expression context, /// and as `int` (i.e. an instance of the class `int`) in type expression context. diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index f850a628bd4e0..07c6aa12c1d7b 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -10,12 +10,14 @@ use crate::{ types::{ BoundTypeVarInstance, ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, MemberLookupPolicy, NominalInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, - Type, TypeVarBoundOrConstraints, TypeVarConstraints, TypeVarInstance, UnionBuilder, + Type, TypeVarBoundOrConstraints, UnionBuilder, constraints::{ConstraintSet, ConstraintSetBuilder}, context::InferContext, diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS}, relation::{HasRelationToVisitor, IsDisjointVisitor}, - todo_type, visitor, + todo_type, + typevar::{TypeVarConstraints, TypeVarInstance}, + visitor, }, }; diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 9095aadf18144..22c6017c03c70 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -44,13 +44,14 @@ use crate::types::generics::{ use crate::types::known_instance::FieldInstance; use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; +use crate::types::typevar::BoundTypeVarIdentity; use crate::types::{ - BoundMethodType, BoundTypeVarIdentity, BoundTypeVarInstance, CallableSignature, CallableType, - CallableTypeKind, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, - EvaluationMode, GenericAlias, InternedConstraintSet, IntersectionType, KnownBoundMethodType, - KnownClass, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, NominalInstanceType, - PropertyInstanceType, SpecialFormType, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, - TypeVarVariance, UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, + BoundMethodType, BoundTypeVarInstance, CallableSignature, CallableType, CallableTypeKind, + ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, EvaluationMode, GenericAlias, + InternedConstraintSet, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, + LiteralValueTypeKind, MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, + SpecialFormType, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, TypeVarVariance, + UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, }; use crate::{DisplaySettings, Program}; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index b75d19c18b4d4..2b011db3b3001 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -80,12 +80,12 @@ use smallvec::SmallVec; use crate::types::class::GenericAlias; use crate::types::generics::InferableTypeVars; +use crate::types::typevar::{BoundTypeVarIdentity, walk_bound_type_var_type}; use crate::types::visitor::{ TypeCollector, TypeVisitor, any_over_type, walk_type_with_recursion_guard, }; use crate::types::{ - BoundTypeVarIdentity, BoundTypeVarInstance, IntersectionType, Type, TypeVarBoundOrConstraints, - UnionType, walk_bound_type_var_type, + BoundTypeVarInstance, IntersectionType, Type, TypeVarBoundOrConstraints, UnionType, }; use crate::{Db, FxIndexMap, FxIndexSet}; diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index a44931bef59e2..20622861afbc7 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -26,12 +26,13 @@ use crate::types::string_annotation::{ }; use crate::types::tuple::TupleSpec; use crate::types::typed_dict::TypedDictSchema; +use crate::types::typevar::TypeVarInstance; use crate::types::{ BoundTypeVarInstance, ClassType, DynamicType, LintDiagnosticGuard, Protocol, ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, protocol_class::ProtocolClass, }; -use crate::types::{KnownInstanceType, MemberLookupPolicy, TypeVarInstance, UnionType}; +use crate::types::{KnownInstanceType, MemberLookupPolicy, UnionType}; use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint}; use itertools::Itertools; use ruff_db::{ diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 2847d135dc5c0..d4d5716e17605 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -29,12 +29,13 @@ use crate::types::signatures::{ CallableSignature, Parameter, Parameters, ParametersKind, Signature, }; use crate::types::tuple::TupleSpec; +use crate::types::typevar::BoundTypeVarIdentity; use crate::types::visitor::TypeVisitor; use crate::types::{ - BindingContext, BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, - KnownBoundMethodType, KnownClass, KnownInstanceType, LiteralValueType, LiteralValueTypeKind, - MaterializationKind, Protocol, ProtocolInstanceType, SpecialFormType, StringLiteralType, - SubclassOfInner, SubclassOfType, Type, TypeAliasType, TypeGuardLike, TypedDictType, UnionType, + BindingContext, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType, + KnownClass, KnownInstanceType, LiteralValueType, LiteralValueTypeKind, MaterializationKind, + Protocol, ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, + SubclassOfType, Type, TypeAliasType, TypeGuardLike, TypedDictType, UnionType, WrapperDescriptorKind, visitor, }; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index d34195041af4a..43687a23d2a8c 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -21,15 +21,17 @@ use crate::types::constraints::{ use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Parameters}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; +use crate::types::typevar::{ + BoundTypeVarIdentity, TypeVarIdentity, TypeVarInstance, walk_type_var_bounds, +}; use crate::types::variance::VarianceInferable; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ - ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, - CallableType, CallableTypes, ClassLiteral, FindLegacyTypeVarsVisitor, IntersectionType, - KnownClass, KnownInstanceType, MaterializationKind, Type, TypeAliasType, TypeContext, - TypeMapping, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, - TypeVarVariance, UnionType, declaration_type, walk_manual_pep_695_type_alias, - walk_pep_695_type_alias, walk_type_var_bounds, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypes, + ClassLiteral, FindLegacyTypeVarsVisitor, IntersectionType, KnownClass, KnownInstanceType, + MaterializationKind, Type, TypeAliasType, TypeContext, TypeMapping, TypeVarBoundOrConstraints, + TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_manual_pep_695_type_alias, + walk_pep_695_type_alias, }; use crate::{Db, FxIndexMap, FxOrderMap, FxOrderSet}; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3a356dd8084ab..dec757b567514 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -126,19 +126,21 @@ use crate::types::typed_dict::{ TypedDictAssignmentKind, TypedDictKeyAssignment, validate_typed_dict_constructor, validate_typed_dict_dict_literal, }; +use crate::types::typevar::{ + BoundTypeVarIdentity, TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, + TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, +}; use crate::types::visitor::find_over_type; use crate::types::{ - BoundTypeVarIdentity, CallDunderError, CallableBinding, CallableType, CallableTypeKind, - ClassType, DataclassParams, DynamicType, EvaluationMode, GenericAlias, InternedConstraintSet, - InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, + CallDunderError, CallableBinding, CallableType, CallableTypeKind, ClassType, DataclassParams, + DynamicType, EvaluationMode, GenericAlias, InternedConstraintSet, InternedType, + IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, LiteralValueTypeKind, ManualPEP695TypeAliasType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, TypeVarDefaultEvaluation, - TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, - UnionType, binding_type, definition_expression_type, infer_complete_scope_types, - infer_scope_types, todo_type, + TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, binding_type, + definition_expression_type, infer_complete_scope_types, infer_scope_types, todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; @@ -9645,13 +9647,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { typevar.identity(self.db()).definition(self.db()), TypeVarKind::Pep613Alias, ); - Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( - self.db(), - identity, - typevar._bound_or_constraints(self.db()), - typevar.explicit_variance(self.db()), - typevar._default(self.db()), - ))) + Type::KnownInstance(KnownInstanceType::TypeVar( + typevar.with_identity(self.db(), identity), + )) } else { inferred_ty }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs index a297449d05d6c..29d3f7f5c72b0 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs @@ -4,10 +4,11 @@ use super::TypeInferenceBuilder; use crate::Db; use crate::types::constraints::ConstraintSetBuilder; use crate::types::diagnostic::{DIVISION_BY_ZERO, report_unsupported_binary_operation}; +use crate::types::typevar::TypeVarConstraints; use crate::types::{ DynamicType, InternedConstraintSet, KnownClass, KnownInstanceType, LiteralValueTypeKind, - MemberLookupPolicy, Type, TypeContext, TypeVarBoundOrConstraints, TypeVarConstraints, - UnionBuilder, UnionTypeInstance, + MemberLookupPolicy, Type, TypeContext, TypeVarBoundOrConstraints, UnionBuilder, + UnionTypeInstance, }; use ruff_python_ast::PythonVersion; diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 33d2fd55eada4..736c72bcc7e56 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -6,11 +6,12 @@ use crate::{ types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassType, GenericContext, InvalidTypeExpressionError, KnownClass, StringLiteralType, Type, TypeAliasType, - TypeContext, TypeMapping, TypeVarInstance, TypeVarVariance, UnionBuilder, + TypeContext, TypeMapping, TypeVarVariance, UnionBuilder, class::NamedTupleSpec, constraints::OwnedConstraintSet, generics::{Specialization, walk_generic_context}, newtype::NewType, + typevar::TypeVarInstance, variance::VarianceInferable, visitor, }, diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs new file mode 100644 index 0000000000000..7dfe530bed89d --- /dev/null +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -0,0 +1,1356 @@ +use std::cell::RefCell; + +use ruff_db::parsed::parsed_module; +use ruff_python_ast::name::Name; +use rustc_hash::FxHashSet; + +use crate::{ + Db, TypeQualifiers, + place::{DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening}, + semantic_index::{ + definition::{Definition, DefinitionKind}, + semantic_index, + }, + types::{ + ApplyTypeMappingVisitor, CycleDetector, DynamicType, KnownClass, KnownInstanceType, + MaterializationKind, Parameter, Parameters, Type, TypeAliasType, TypeContext, TypeMapping, + TypeVarVariance, UnionBuilder, UnionType, any_over_type, binding_type, + definition_expression_type, tuple::Tuple, variance::VarianceInferable, visitor, + }, +}; + +impl<'db> Type<'db> { + pub(crate) const fn is_type_var(self) -> bool { + matches!(self, Type::TypeVar(_)) + } + + pub(crate) const fn as_typevar(self) -> Option> { + match self { + Type::TypeVar(bound_typevar) => Some(bound_typevar), + _ => None, + } + } + + pub(crate) fn has_typevar(self, db: &'db dyn Db) -> bool { + any_over_type(db, self, false, |ty| matches!(ty, Type::TypeVar(_))) + } + + pub(crate) fn has_non_self_typevar(self, db: &'db dyn Db) -> bool { + any_over_type( + db, + self, + false, + |ty| matches!(ty, Type::TypeVar(tv) if !tv.typevar(db).is_self(db)), + ) + } + + pub(crate) fn has_typevar_or_typevar_instance(self, db: &'db dyn Db) -> bool { + any_over_type(db, self, false, |ty| { + matches!( + ty, + Type::KnownInstance(KnownInstanceType::TypeVar(_)) | Type::TypeVar(_) + ) + }) + } + + pub(crate) fn has_unspecialized_type_var(self, db: &'db dyn Db) -> bool { + any_over_type(db, self, false, |ty| { + matches!(ty, Type::Dynamic(DynamicType::UnspecializedTypeVar)) + }) + } +} + +/// A specific instance of a type variable that has not been bound to a generic context yet. +/// +/// This is usually not the type that you want; if you are working with a typevar, in a generic +/// context, which might be specialized to a concrete type, you want [`BoundTypeVarInstance`]. This +/// type holds information that does not depend on which generic context the typevar is used in. +/// +/// For a legacy typevar: +/// +/// ```py +/// T = TypeVar("T") # [1] +/// def generic_function(t: T) -> T: ... # [2] +/// ``` +/// +/// we will create a `TypeVarInstance` for the typevar `T` when it is instantiated. The type of `T` +/// at `[1]` will be a `KnownInstanceType::TypeVar` wrapping this `TypeVarInstance`. The typevar is +/// not yet bound to any generic context at this point. +/// +/// The typevar is used in `generic_function`, which binds it to a new generic context. We will +/// create a [`BoundTypeVarInstance`] for this new binding of the typevar. The type of `T` at `[2]` +/// will be a `Type::TypeVar` wrapping this `BoundTypeVarInstance`. +/// +/// For a PEP 695 typevar: +/// +/// ```py +/// def generic_function[T](t: T) -> T: ... +/// # ╰─────╰─────────── [2] +/// # ╰─────────────────────── [1] +/// ``` +/// +/// the typevar is defined and immediately bound to a single generic context. Just like in the +/// legacy case, we will create a `TypeVarInstance` and [`BoundTypeVarInstance`], and the type of +/// `T` at `[1]` and `[2]` will be that `TypeVarInstance` and `BoundTypeVarInstance`, respectively. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct TypeVarInstance<'db> { + /// The identity of this typevar + pub(crate) identity: TypeVarIdentity<'db>, + + /// The upper bound or constraint on the type of this TypeVar, if any. Don't use this field + /// directly; use the `bound_or_constraints` (or `upper_bound` and `constraints`) methods + /// instead (to evaluate any lazy bound or constraints). + _bound_or_constraints: Option>, + + /// The explicitly specified variance of the TypeVar + pub(super) explicit_variance: Option, + + /// The default type for this TypeVar, if any. Don't use this field directly, use the + /// `default_type` method instead (to evaluate any lazy default). + _default: Option>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for TypeVarInstance<'_> {} + +pub(super) fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + typevar: TypeVarInstance<'db>, + visitor: &V, +) { + if let Some(bound_or_constraints) = if visitor.should_visit_lazy_type_attributes() { + typevar.bound_or_constraints(db) + } else { + match typevar._bound_or_constraints(db) { + _ if visitor.should_visit_lazy_type_attributes() => typevar.bound_or_constraints(db), + Some(TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints)) => { + Some(bound_or_constraints) + } + _ => None, + } + } { + walk_type_var_bounds(db, bound_or_constraints, visitor); + } + if let Some(default_type) = if visitor.should_visit_lazy_type_attributes() { + typevar.default_type(db) + } else { + match typevar._default(db) { + Some(TypeVarDefaultEvaluation::Eager(default_type)) => Some(default_type), + _ => None, + } + } { + visitor.visit_type(db, default_type); + } +} + +#[salsa::tracked] +impl<'db> TypeVarInstance<'db> { + pub(crate) fn with_binding_context( + self, + db: &'db dyn Db, + binding_context: Definition<'db>, + ) -> BoundTypeVarInstance<'db> { + BoundTypeVarInstance::new(db, self, BindingContext::Definition(binding_context), None) + } + + fn with_name_suffix(self, db: &'db dyn Db, suffix: &str) -> Self { + Self::new( + db, + self.identity(db).with_name_suffix(db, suffix), + self._bound_or_constraints(db), + self.explicit_variance(db), + self._default(db), + ) + } + + pub(super) fn with_identity(self, db: &'db dyn Db, identity: TypeVarIdentity<'db>) -> Self { + Self::new( + db, + identity, + self._bound_or_constraints(db), + self.explicit_variance(db), + self._default(db), + ) + } + + pub(crate) fn name(self, db: &'db dyn Db) -> &'db Name { + self.identity(db).name(db) + } + + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + self.identity(db).definition(db) + } + + pub fn kind(self, db: &'db dyn Db) -> TypeVarKind { + self.identity(db).kind(db) + } + + pub(crate) fn is_self(self, db: &'db dyn Db) -> bool { + matches!(self.kind(db), TypeVarKind::TypingSelf) + } + + pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { + self.kind(db).is_paramspec() + } + + pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { + if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { + Some(ty) + } else { + None + } + } + + pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&'db [Type<'db>]> { + if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { + Some(tuple.elements(db)) + } else { + None + } + } + + pub(crate) fn bound_or_constraints( + self, + db: &'db dyn Db, + ) -> Option> { + self._bound_or_constraints(db).and_then(|w| match w { + TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { + Some(bound_or_constraints) + } + TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self + .lazy_bound(db) + .map(TypeVarBoundOrConstraints::UpperBound), + TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self + .lazy_constraints(db) + .map(TypeVarBoundOrConstraints::Constraints), + }) + } + + /// Returns the bounds or constraints of this typevar. If the typevar is unbounded, returns + /// `object` as its upper bound. + pub(crate) fn require_bound_or_constraints( + self, + db: &'db dyn Db, + ) -> TypeVarBoundOrConstraints<'db> { + self.bound_or_constraints(db) + .unwrap_or_else(|| TypeVarBoundOrConstraints::UpperBound(Type::object())) + } + + pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { + let visitor = TypeVarDefaultVisitor::new(None); + self.default_type_impl(db, &visitor) + } + + fn default_type_impl( + self, + db: &'db dyn Db, + visitor: &TypeVarDefaultVisitor<'db>, + ) -> Option> { + visitor.visit(self, || { + self._default(db).and_then(|default| match default { + TypeVarDefaultEvaluation::Eager(ty) => Some(ty), + TypeVarDefaultEvaluation::Lazy => self.lazy_default_impl(db, visitor), + }) + }) + } + + fn materialize_impl( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + Self::new( + db, + self.identity(db), + self._bound_or_constraints(db) + .and_then(|bound_or_constraints| match bound_or_constraints { + TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => Some( + bound_or_constraints + .materialize_impl(db, materialization_kind, visitor) + .into(), + ), + TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => { + self.lazy_bound(db).map(|bound| { + TypeVarBoundOrConstraints::UpperBound(bound) + .materialize_impl(db, materialization_kind, visitor) + .into() + }) + } + TypeVarBoundOrConstraintsEvaluation::LazyConstraints => { + self.lazy_constraints(db).map(|constraints| { + TypeVarBoundOrConstraints::Constraints(constraints) + .materialize_impl(db, materialization_kind, visitor) + .into() + }) + } + }), + self.explicit_variance(db), + self._default(db).and_then(|default| match default { + TypeVarDefaultEvaluation::Eager(ty) => { + Some(ty.materialize(db, materialization_kind, visitor).into()) + } + TypeVarDefaultEvaluation::Lazy => self + .lazy_default(db) + .map(|ty| ty.materialize(db, materialization_kind, visitor).into()), + }), + ) + } + + fn to_instance(self, db: &'db dyn Db) -> Option { + let bound_or_constraints = match self.bound_or_constraints(db)? { + TypeVarBoundOrConstraints::UpperBound(upper_bound) => { + TypeVarBoundOrConstraints::UpperBound(upper_bound.to_instance(db)?) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + TypeVarBoundOrConstraints::Constraints(constraints.to_instance(db)?) + } + }; + let identity = TypeVarIdentity::new( + db, + Name::new(format!("{}'instance", self.name(db))), + None, // definition + self.kind(db), + ); + Some(Self::new( + db, + identity, + Some(bound_or_constraints.into()), + self.explicit_variance(db), + None, // _default + )) + } + + fn type_is_self_referential( + self, + db: &'db dyn Db, + ty: Type<'db>, + visitor: &TypeVarDefaultVisitor<'db>, + ) -> bool { + #[derive(Copy, Clone)] + struct State<'db, 'a> { + db: &'db dyn Db, + visitor: &'a TypeVarDefaultVisitor<'db>, + seen_typevars: &'a RefCell>>, + seen_type_aliases: &'a RefCell>>, + } + + fn typevar_default_is_self_referential<'db>( + state: State<'db, '_>, + typevar: TypeVarInstance<'db>, + self_identity: TypeVarIdentity<'db>, + ) -> bool { + if typevar.identity(state.db) == self_identity { + return true; + } + + if !state.seen_typevars.borrow_mut().insert(typevar) { + return false; + } + + typevar + .default_type_impl(state.db, state.visitor) + .is_some_and(|default_ty| { + type_is_self_referential_impl(state, default_ty, self_identity) + }) + } + + fn type_alias_is_self_referential<'db>( + state: State<'db, '_>, + type_alias: TypeAliasType<'db>, + self_identity: TypeVarIdentity<'db>, + ) -> bool { + if !state.seen_type_aliases.borrow_mut().insert(type_alias) { + return false; + } + + type_is_self_referential_impl(state, type_alias.raw_value_type(state.db), self_identity) + } + + fn type_is_self_referential_impl<'db>( + state: State<'db, '_>, + ty: Type<'db>, + self_identity: TypeVarIdentity<'db>, + ) -> bool { + any_over_type(state.db, ty, false, |inner_ty| match inner_ty { + Type::TypeVar(bound_typevar) => typevar_default_is_self_referential( + state, + bound_typevar.typevar(state.db), + self_identity, + ), + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { + typevar_default_is_self_referential(state, typevar, self_identity) + } + Type::TypeAlias(alias) => { + type_alias_is_self_referential(state, alias, self_identity) + } + Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => { + type_alias_is_self_referential(state, alias, self_identity) + } + _ => false, + }) + } + + let seen_typevars = RefCell::new(FxHashSet::default()); + let seen_type_aliases = RefCell::new(FxHashSet::default()); + + let state = State { + db, + visitor, + seen_typevars: &seen_typevars, + seen_type_aliases: &seen_type_aliases, + }; + + type_is_self_referential_impl(state, ty, self.identity(db)) + } + + /// Returns the "unchecked" upper bound of a type variable instance. + /// `lazy_bound` checks if the upper bound type is generic (generic upper bound is not allowed). + #[salsa::tracked( + cycle_fn=lazy_bound_cycle_recover, + cycle_initial=|_, _, _| None, + heap_size=ruff_memory_usage::heap_size + )] + fn lazy_bound_unchecked(self, db: &'db dyn Db) -> Option> { + let definition = self.definition(db)?; + let module = parsed_module(db, definition.file(db)).load(db); + let ty = match definition.kind(db) { + // PEP 695 typevar + DefinitionKind::TypeVar(typevar) => { + let typevar_node = typevar.node(&module); + definition_expression_type(db, definition, typevar_node.bound.as_ref()?) + } + // legacy typevar + DefinitionKind::Assignment(assignment) => { + let call_expr = assignment.value(&module).as_call_expr()?; + let expr = &call_expr.arguments.find_keyword("bound")?.value; + definition_expression_type(db, definition, expr) + } + _ => return None, + }; + + Some(ty) + } + + fn lazy_bound(self, db: &'db dyn Db) -> Option> { + let bound = self.lazy_bound_unchecked(db)?; + + if bound.has_typevar_or_typevar_instance(db) { + return None; + } + + Some(bound) + } + + /// Returns the "unchecked" constraints of a type variable instance. + /// `lazy_constraints` checks if any of the constraint types are generic (generic constraints are not allowed). + #[salsa::tracked( + cycle_fn=lazy_constraints_cycle_recover, + cycle_initial=|_, _, _| None, + heap_size=ruff_memory_usage::heap_size + )] + fn lazy_constraints_unchecked(self, db: &'db dyn Db) -> Option> { + let definition = self.definition(db)?; + let module = parsed_module(db, definition.file(db)).load(db); + let constraints = match definition.kind(db) { + // PEP 695 typevar + DefinitionKind::TypeVar(typevar) => { + let typevar_node = typevar.node(&module); + let bound = + definition_expression_type(db, definition, typevar_node.bound.as_ref()?); + let constraints = if let Some(tuple) = bound.tuple_instance_spec(db) + && let Tuple::Fixed(tuple) = tuple.into_owned() + { + tuple.owned_elements() + } else { + vec![Type::unknown()].into_boxed_slice() + }; + TypeVarConstraints::new(db, constraints) + } + // legacy typevar + DefinitionKind::Assignment(assignment) => { + let call_expr = assignment.value(&module).as_call_expr()?; + TypeVarConstraints::new( + db, + call_expr + .arguments + .args + .iter() + .skip(1) + .map(|arg| definition_expression_type(db, definition, arg)) + .collect::>(), + ) + } + _ => return None, + }; + + Some(constraints) + } + + fn lazy_constraints(self, db: &'db dyn Db) -> Option> { + let constraints = self.lazy_constraints_unchecked(db)?; + + if constraints + .elements(db) + .iter() + .any(|ty| ty.has_typevar_or_typevar_instance(db)) + { + return None; + } + + Some(constraints) + } + + /// Returns the "unchecked" default type of a type variable instance. + /// `lazy_default` checks if the default type is not self-referential. + #[salsa::tracked(cycle_initial=|_, id, _| Some(Type::divergent(id)), cycle_fn=lazy_default_cycle_recover, heap_size=ruff_memory_usage::heap_size)] + fn lazy_default_unchecked(self, db: &'db dyn Db) -> Option> { + fn convert_type_to_paramspec_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + let parameters = match ty { + Type::NominalInstance(nominal_instance) + if nominal_instance.has_known_class(db, KnownClass::EllipsisType) => + { + Parameters::gradual_form() + } + Type::NominalInstance(nominal_instance) => nominal_instance + .own_tuple_spec(db) + .map_or_else(Parameters::unknown, |tuple_spec| { + Parameters::new( + db, + tuple_spec + .iter_all_elements() + .map(|ty| Parameter::positional_only(None).with_annotated_type(ty)), + ) + }), + Type::Dynamic(dynamic) => match dynamic { + DynamicType::Todo(_) + | DynamicType::TodoUnpack + | DynamicType::TodoStarredExpression + | DynamicType::TodoFunctionalTypedDict + | DynamicType::TodoTypeVarTuple => Parameters::todo(), + DynamicType::Any + | DynamicType::Unknown + | DynamicType::UnknownGeneric(_) + | DynamicType::UnspecializedTypeVar + | DynamicType::Divergent(_) => Parameters::unknown(), + }, + Type::TypeVar(typevar) if typevar.is_paramspec(db) => { + return ty; + } + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) + if typevar.is_paramspec(db) => + { + return ty; + } + _ => Parameters::unknown(), + }; + Type::paramspec_value_callable(db, parameters) + } + + let definition = self.definition(db)?; + let module = parsed_module(db, definition.file(db)).load(db); + let ty = match definition.kind(db) { + // PEP 695 typevar + DefinitionKind::TypeVar(typevar) => { + let typevar_node = typevar.node(&module); + definition_expression_type(db, definition, typevar_node.default.as_ref()?) + } + // legacy typevar / ParamSpec + DefinitionKind::Assignment(assignment) => { + let call_expr = assignment.value(&module).as_call_expr()?; + let func_ty = definition_expression_type(db, definition, &call_expr.func); + let known_class = func_ty.as_class_literal().and_then(|cls| cls.known(db)); + let expr = &call_expr.arguments.find_keyword("default")?.value; + let default_type = definition_expression_type(db, definition, expr); + if known_class == Some(KnownClass::ParamSpec) { + convert_type_to_paramspec_value(db, default_type) + } else { + default_type + } + } + // PEP 695 ParamSpec + DefinitionKind::ParamSpec(paramspec) => { + let paramspec_node = paramspec.node(&module); + let default_ty = + definition_expression_type(db, definition, paramspec_node.default.as_ref()?); + convert_type_to_paramspec_value(db, default_ty) + } + _ => return None, + }; + + Some(ty) + } + + fn lazy_default(self, db: &'db dyn Db) -> Option> { + let visitor = TypeVarDefaultVisitor::new(None); + self.lazy_default_impl(db, &visitor) + } + + fn lazy_default_impl( + self, + db: &'db dyn Db, + visitor: &TypeVarDefaultVisitor<'db>, + ) -> Option> { + let default = self.lazy_default_unchecked(db)?; + + // Unlike bounds/constraints, default types are allowed to be generic (https://peps.python.org/pep-0696/#using-another-type-parameter-as-default). + // Here we simply check for non-self-referential. + // TODO: We should also check for non-forward references. + if self.type_is_self_referential(db, default, visitor) { + return None; + } + + Some(default) + } + + pub fn bind_pep695(self, db: &'db dyn Db) -> Option> { + if !matches!( + self.identity(db).kind(db), + TypeVarKind::Pep695 | TypeVarKind::Pep695ParamSpec + ) { + return None; + } + let typevar_definition = self.definition(db)?; + let index = semantic_index(db, typevar_definition.file(db)); + let (_, child) = index + .child_scopes(typevar_definition.file_scope(db)) + .next()?; + child + .node() + .generic_context(db, index)? + .binds_typevar(db, self) + } +} + +/// A type variable that has been bound to a generic context, and which can be specialized to a +/// concrete type. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct BoundTypeVarInstance<'db> { + pub typevar: TypeVarInstance<'db>, + pub(super) binding_context: BindingContext<'db>, + /// If [`Some`], this indicates that this type variable is the `args` or `kwargs` component + /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. + pub(super) paramspec_attr: Option, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for BoundTypeVarInstance<'_> {} + +impl<'db> BoundTypeVarInstance<'db> { + pub(crate) fn with_name_suffix(self, db: &'db dyn Db, suffix: &str) -> Self { + Self::new( + db, + self.typevar(db).with_name_suffix(db, suffix), + self.binding_context(db), + self.paramspec_attr(db), + ) + } + + /// Get the identity of this bound typevar. + /// + /// This is used for comparing whether two bound typevars represent the same logical typevar, + /// regardless of e.g. differences in their bounds or constraints due to materialization. + pub(crate) fn identity(self, db: &'db dyn Db) -> BoundTypeVarIdentity<'db> { + BoundTypeVarIdentity { + identity: self.typevar(db).identity(db), + binding_context: self.binding_context(db), + paramspec_attr: self.paramspec_attr(db), + } + } + + pub(crate) fn name(self, db: &'db dyn Db) -> &'db Name { + self.typevar(db).name(db) + } + + pub(crate) fn kind(self, db: &'db dyn Db) -> TypeVarKind { + self.typevar(db).kind(db) + } + + pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { + self.kind(db).is_paramspec() + } + + /// Returns a new bound typevar instance with the given `ParamSpec` attribute set. + /// + /// This method will also set an appropriate upper bound on the typevar, based on the + /// attribute kind. For `P.args`, the upper bound will be `tuple[object, ...]`, and for + /// `P.kwargs`, the upper bound will be `Top[dict[str, Any]]`. + /// + /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` + /// type variable. + pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { + debug_assert!( + self.is_paramspec(db), + "Expected a ParamSpec, got {:?}", + self.kind(db) + ); + + let upper_bound = TypeVarBoundOrConstraints::UpperBound(match kind { + ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, Type::object()), + ParamSpecAttrKind::Kwargs => KnownClass::Dict + .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]) + .top_materialization(db), + }); + + let typevar = TypeVarInstance::new( + db, + self.typevar(db).identity(db), + Some(TypeVarBoundOrConstraintsEvaluation::Eager(upper_bound)), + None, // ParamSpecs cannot have explicit variance + None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can + ); + + Self::new(db, typevar, self.binding_context(db), Some(kind)) + } + + /// Returns a new bound typevar instance without any `ParamSpec` attribute set. + /// + /// This method will also remove any upper bound that was set by `with_paramspec_attr`. This + /// means that the returned typevar will have no upper bound or constraints. + /// + /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` + /// type variable. + pub(crate) fn without_paramspec_attr(self, db: &'db dyn Db) -> Self { + debug_assert!( + self.is_paramspec(db), + "Expected a ParamSpec, got {:?}", + self.kind(db) + ); + + Self::new( + db, + TypeVarInstance::new( + db, + self.typevar(db).identity(db), + None, // Remove the upper bound set by `with_paramspec_attr` + None, // ParamSpecs cannot have explicit variance + None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can + ), + self.binding_context(db), + None, + ) + } + + /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g. + /// differences in their bounds or constraints due to materialization. + pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { + self.identity(db) == other.identity(db) + } + + /// Create a new PEP 695 type variable that can be used in signatures + /// of synthetic generic functions. + pub(crate) fn synthetic(db: &'db dyn Db, name: Name, variance: TypeVarVariance) -> Self { + let identity = TypeVarIdentity::new( + db, + name, + None, // definition + TypeVarKind::Pep695, + ); + let typevar = TypeVarInstance::new( + db, + identity, + None, // _bound_or_constraints + Some(variance), + None, // _default + ); + Self::new(db, typevar, BindingContext::Synthetic, None) + } + + /// Create a new synthetic `Self` type variable with the given upper bound. + pub(crate) fn synthetic_self( + db: &'db dyn Db, + upper_bound: Type<'db>, + binding_context: BindingContext<'db>, + ) -> Self { + let identity = TypeVarIdentity::new( + db, + Name::new_static("Self"), + None, // definition + TypeVarKind::TypingSelf, + ); + let typevar = TypeVarInstance::new( + db, + identity, + Some(TypeVarBoundOrConstraints::UpperBound(upper_bound).into()), + Some(TypeVarVariance::Invariant), + None, // _default + ); + Self::new(db, typevar, binding_context, None) + } + + /// Returns an identical type variable with its `TypeVarBoundOrConstraints` mapped by the + /// provided closure. + pub(crate) fn map_bound_or_constraints( + self, + db: &'db dyn Db, + f: impl FnOnce(Option>) -> Option>, + ) -> Self { + let bound_or_constraints = f(self.typevar(db).bound_or_constraints(db)); + let typevar = TypeVarInstance::new( + db, + self.typevar(db).identity(db), + bound_or_constraints.map(TypeVarBoundOrConstraintsEvaluation::Eager), + self.typevar(db).explicit_variance(db), + self.typevar(db)._default(db), + ); + + Self::new( + db, + typevar, + self.binding_context(db), + self.paramspec_attr(db), + ) + } + + pub(crate) fn variance_with_polarity( + self, + db: &'db dyn Db, + polarity: TypeVarVariance, + ) -> TypeVarVariance { + let _span = tracing::trace_span!("variance_with_polarity").entered(); + match self.typevar(db).explicit_variance(db) { + Some(explicit_variance) => explicit_variance.compose(polarity), + None => match self.binding_context(db) { + BindingContext::Definition(definition) => binding_type(db, definition) + .with_polarity(polarity) + .variance_of(db, self), + BindingContext::Synthetic => TypeVarVariance::Invariant, + }, + } + } + + pub fn variance(self, db: &'db dyn Db) -> TypeVarVariance { + self.variance_with_polarity(db, TypeVarVariance::Covariant) + } + + pub(super) fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Type<'db> { + match type_mapping { + TypeMapping::ApplySpecialization(specialization) => { + let typevar = if self.is_paramspec(db) { + self.without_paramspec_attr(db) + } else { + self + }; + specialization + .get(db, typevar) + .map(|ty| { + if let Some(attr) = self.paramspec_attr(db) + && let Type::TypeVar(typevar) = ty + && typevar.is_paramspec(db) + { + return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); + } + ty + }) + .unwrap_or(Type::TypeVar(self)) + } + TypeMapping::BindSelf(binding) => { + if binding.should_bind(db, self) { + binding.self_type() + } else { + Type::TypeVar(self) + } + } + TypeMapping::ReplaceSelf { new_upper_bound } => { + if self.typevar(db).is_self(db) { + Type::TypeVar(BoundTypeVarInstance::synthetic_self( + db, + *new_upper_bound, + self.binding_context(db), + )) + } else { + Type::TypeVar(self) + } + } + TypeMapping::UniqueSpecialization { .. } + | TypeMapping::PromoteLiterals(_) + | TypeMapping::ReplaceParameterDefaults + | TypeMapping::BindLegacyTypevars(_) + | TypeMapping::EagerExpansion + | TypeMapping::RescopeReturnCallables(_) => Type::TypeVar(self), + TypeMapping::Materialize(materialization_kind) => { + Type::TypeVar(self.materialize_impl(db, *materialization_kind, visitor)) + } + } + } +} + +pub(super) fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, + visitor: &V, +) { + visitor.visit_type_var_type(db, bound_typevar.typevar(db)); +} + +impl<'db> BoundTypeVarInstance<'db> { + /// Returns the default value of this typevar, recursively applying its binding context to any + /// other typevars that appear in the default. + /// + /// For instance, in + /// + /// ```py + /// T = TypeVar("T") + /// U = TypeVar("U", default=T) + /// + /// # revealed: typing.TypeVar[U = typing.TypeVar[T]] + /// reveal_type(U) + /// + /// # revealed: typing.Generic[T, U = T@C] + /// class C(reveal_type(Generic[T, U])): ... + /// ``` + /// + /// In the first case, the use of `U` is unbound, and so we have a [`TypeVarInstance`], and its + /// default value (`T`) is also unbound. + /// + /// By using `U` in the generic class, it becomes bound, and so we have a + /// `BoundTypeVarInstance`. As part of binding `U` we must also bind its default value + /// (resulting in `T@C`). + pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { + bound_typevar_default_type(db, self) + } + + fn materialize_impl( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + Self::new( + db, + self.typevar(db) + .materialize_impl(db, materialization_kind, visitor), + self.binding_context(db), + self.paramspec_attr(db), + ) + } + + pub(super) fn to_instance(self, db: &'db dyn Db) -> Option { + Some(Self::new( + db, + self.typevar(db).to_instance(db)?, + self.binding_context(db), + self.paramspec_attr(db), + )) + } +} + +/// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax, +/// or an implicit typevar like `Self` was used. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +pub enum TypeVarKind { + /// `T = TypeVar("T")` + Legacy, + /// `def foo[T](x: T) -> T: ...` + Pep695, + /// `typing.Self` + TypingSelf, + /// `P = ParamSpec("P")` + ParamSpec, + /// `def foo[**P]() -> None: ...` + Pep695ParamSpec, + /// `Alias: typing.TypeAlias = T` + Pep613Alias, +} + +impl TypeVarKind { + pub(super) const fn is_self(self) -> bool { + matches!(self, Self::TypingSelf) + } + + pub(super) const fn is_paramspec(self) -> bool { + matches!(self, Self::ParamSpec | Self::Pep695ParamSpec) + } +} + +/// The identity of a type variable. +/// +/// This represents the core identity of a typevar, independent of its bounds or constraints. Two +/// typevars have the same identity if they represent the same logical typevar, even if their +/// bounds have been materialized differently. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct TypeVarIdentity<'db> { + /// The name of this TypeVar (e.g. `T`) + #[returns(ref)] + pub(crate) name: Name, + + /// The type var's definition (None if synthesized) + pub(crate) definition: Option>, + + /// The kind of typevar (PEP 695, Legacy, or TypingSelf) + pub(crate) kind: TypeVarKind, +} + +impl get_size2::GetSize for TypeVarIdentity<'_> {} + +impl<'db> TypeVarIdentity<'db> { + fn with_name_suffix(self, db: &'db dyn Db, suffix: &str) -> Self { + let name = format!("{}'{}", self.name(db), suffix); + Self::new(db, Name::from(name), self.definition(db), self.kind(db)) + } +} + +#[expect(clippy::ref_option)] +fn lazy_bound_cycle_recover<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + previous: &Option>, + current: Option>, + _typevar: TypeVarInstance<'db>, +) -> Option> { + // Normalize the bounds/constraints to ensure cycle convergence. + match (previous, current) { + (Some(prev), Some(current)) => Some(current.cycle_normalized(db, *prev, cycle)), + (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), + (_, None) => None, + } +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +#[expect(clippy::ref_option)] +fn lazy_constraints_cycle_recover<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + previous: &Option>, + current: Option>, + _typevar: TypeVarInstance<'db>, +) -> Option> { + // Normalize the bounds/constraints to ensure cycle convergence. + match (previous, current) { + (Some(prev), Some(constraints)) => Some(constraints.cycle_normalized(db, *prev, cycle)), + (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), + (_, None) => None, + } +} + +#[expect(clippy::ref_option)] +fn lazy_default_cycle_recover<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + previous_default: &Option>, + default: Option>, + _typevar: TypeVarInstance<'db>, +) -> Option> { + // Normalize the default to ensure cycle convergence. + match (previous_default, default) { + (Some(prev), Some(default)) => Some(default.cycle_normalized(db, *prev, cycle)), + (None, Some(default)) => Some(default.recursive_type_normalized(db, cycle)), + (_, None) => None, + } +} + +/// Where a type variable is bound and usable. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] +pub enum BindingContext<'db> { + /// The definition of the generic class, function, or type alias that binds this typevar. + Definition(Definition<'db>), + /// The typevar is synthesized internally, and is not associated with a particular definition + /// in the source, but is still bound and eligible for specialization inference. + Synthetic, +} + +impl<'db> From> for BindingContext<'db> { + fn from(definition: Definition<'db>) -> Self { + BindingContext::Definition(definition) + } +} + +impl<'db> BindingContext<'db> { + pub(crate) fn definition(self) -> Option> { + match self { + BindingContext::Definition(definition) => Some(definition), + BindingContext::Synthetic => None, + } + } + + pub(super) fn name(self, db: &'db dyn Db) -> Option { + self.definition().and_then(|definition| definition.name(db)) + } +} + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, get_size2::GetSize)] +pub enum ParamSpecAttrKind { + Args, + Kwargs, +} + +impl std::fmt::Display for ParamSpecAttrKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParamSpecAttrKind::Args => f.write_str("args"), + ParamSpecAttrKind::Kwargs => f.write_str("kwargs"), + } + } +} + +/// The identity of a bound type variable. +/// +/// This identifies a specific binding of a typevar to a context (e.g., `T@ClassC` vs `T@FunctionF`), +/// independent of the typevar's bounds or constraints. Two bound typevars have the same identity +/// if they represent the same logical typevar bound in the same context, even if their bounds +/// have been materialized differently. +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] +pub struct BoundTypeVarIdentity<'db> { + pub(crate) identity: TypeVarIdentity<'db>, + pub(crate) binding_context: BindingContext<'db>, + /// If [`Some`], this indicates that this type variable is the `args` or `kwargs` component + /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. + pub(super) paramspec_attr: Option, +} + +#[salsa::tracked( + cycle_initial=|_, _, _| None, + cycle_fn=bound_typevar_default_type_cycle_recover, + heap_size=ruff_memory_usage::heap_size +)] +fn bound_typevar_default_type<'db>( + db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, +) -> Option> { + let binding_context = bound_typevar.binding_context(db); + bound_typevar.typevar(db).default_type(db).map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::BindLegacyTypevars(binding_context), + TypeContext::default(), + ) + }) +} + +#[expect(clippy::ref_option)] +fn bound_typevar_default_type_cycle_recover<'db>( + _db: &'db dyn Db, + _cycle: &salsa::Cycle, + _previous_default: &Option>, + _default: Option>, + _bound_typevar: BoundTypeVarInstance<'db>, +) -> Option> { + None +} + +/// Whether a typevar default is eagerly specified or lazily evaluated. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub enum TypeVarDefaultEvaluation<'db> { + /// The default type is lazily evaluated. + Lazy, + /// The default type is eagerly specified. + Eager(Type<'db>), +} + +impl<'db> From> for TypeVarDefaultEvaluation<'db> { + fn from(value: Type<'db>) -> Self { + TypeVarDefaultEvaluation::Eager(value) + } +} + +/// Whether a typevar bound/constraints is eagerly specified or lazily evaluated. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub enum TypeVarBoundOrConstraintsEvaluation<'db> { + /// There is a lazily-evaluated upper bound. + LazyUpperBound, + /// There is a lazily-evaluated set of constraints. + LazyConstraints, + /// The upper bound/constraints are eagerly specified. + Eager(TypeVarBoundOrConstraints<'db>), +} + +impl<'db> From> for TypeVarBoundOrConstraintsEvaluation<'db> { + fn from(value: TypeVarBoundOrConstraints<'db>) -> Self { + TypeVarBoundOrConstraintsEvaluation::Eager(value) + } +} + +/// Type variable constraints (e.g. `T: (int, str)`). +/// This is structurally identical to [`UnionType`], except that it does not perform simplification and preserves the element types. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct TypeVarConstraints<'db> { + #[returns(ref)] + pub(super) elements: Box<[Type<'db>]>, +} + +impl get_size2::GetSize for TypeVarConstraints<'_> {} + +fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + constraints: TypeVarConstraints<'db>, + visitor: &V, +) { + for ty in constraints.elements(db) { + visitor.visit_type(db, *ty); + } +} + +impl<'db> TypeVarConstraints<'db> { + pub(super) fn as_type(self, db: &'db dyn Db) -> Type<'db> { + UnionType::from_elements(db, self.elements(db)) + } + + fn to_instance(self, db: &'db dyn Db) -> Option> { + let mut instance_elements = Vec::new(); + for ty in self.elements(db) { + instance_elements.push(ty.to_instance(db)?); + } + Some(TypeVarConstraints::new( + db, + instance_elements.into_boxed_slice(), + )) + } + + pub(super) fn map( + self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, + ) -> Self { + let mapped = self + .elements(db) + .iter() + .map(transform_fn) + .collect::>(); + TypeVarConstraints::new(db, mapped) + } + + pub(crate) fn map_with_boundness_and_qualifiers( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { + let mut builder = UnionBuilder::new(db); + let mut qualifiers = TypeQualifiers::empty(); + + let mut all_unbound = true; + let mut possibly_unbound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.elements(db) { + let PlaceAndQualifiers { + place: ty_member, + qualifiers: new_qualifiers, + } = transform_fn(ty); + qualifiers |= new_qualifiers; + match ty_member { + Place::Undefined => { + possibly_unbound = true; + } + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + if member_boundness == Definedness::PossiblyUndefined { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + PlaceAndQualifiers { + place: if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder.build(), + origin, + definedness: if possibly_unbound { + Definedness::PossiblyUndefined + } else { + Definedness::AlwaysDefined + }, + widening: Widening::None, + }) + }, + qualifiers, + } + } + + fn materialize_impl( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + let materialized = self + .elements(db) + .iter() + .map(|ty| ty.materialize(db, materialization_kind, visitor)) + .collect::>(); + TypeVarConstraints::new(db, materialized) + } + + /// Normalize for cycle recovery by combining with the previous value and + /// removing divergent types introduced by the cycle. + /// + /// See [`Type::cycle_normalized`] for more details on how this works. + fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + let current_elements = self.elements(db); + let prev_elements = previous.elements(db); + TypeVarConstraints::new( + db, + current_elements + .iter() + .zip(prev_elements.iter()) + .map(|(ty, prev_ty)| ty.cycle_normalized(db, *prev_ty, cycle)) + .collect::>(), + ) + } + + /// Normalize recursive types for cycle recovery when there's no previous value. + /// + /// See [`Type::recursive_type_normalized`] for more details. + fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { + self.map(db, |ty| ty.recursive_type_normalized(db, cycle)) + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub enum TypeVarBoundOrConstraints<'db> { + UpperBound(Type<'db>), + Constraints(TypeVarConstraints<'db>), +} + +pub(super) fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + bounds: TypeVarBoundOrConstraints<'db>, + visitor: &V, +) { + match bounds { + TypeVarBoundOrConstraints::UpperBound(bound) => visitor.visit_type(db, bound), + TypeVarBoundOrConstraints::Constraints(constraints) => { + walk_type_var_constraints(db, constraints, visitor); + } + } +} + +impl<'db> TypeVarBoundOrConstraints<'db> { + fn materialize_impl( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + match self { + TypeVarBoundOrConstraints::UpperBound(bound) => TypeVarBoundOrConstraints::UpperBound( + bound.materialize(db, materialization_kind, visitor), + ), + TypeVarBoundOrConstraints::Constraints(constraints) => { + TypeVarBoundOrConstraints::Constraints(constraints.materialize_impl( + db, + materialization_kind, + visitor, + )) + } + } + } +} + +/// A [`CycleDetector`] that is used in `TypeVarInstance::default_type`. +pub(crate) type TypeVarDefaultVisitor<'db> = + CycleDetector, Option>>; +pub(crate) struct VisitTypeVarDefault; diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 4819477fe7007..d457daccac452 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -6,7 +6,7 @@ use crate::{ BoundMethodType, BoundSuperType, BoundTypeVarInstance, CallableType, GenericAlias, IntersectionType, KnownBoundMethodType, KnownInstanceType, NominalInstanceType, PropertyInstanceType, ProtocolInstanceType, SubclassOfType, Type, TypeAliasType, - TypeGuardType, TypeIsType, TypeVarInstance, TypedDictType, UnionType, + TypeGuardType, TypeIsType, TypedDictType, UnionType, bound_super::walk_bound_super_type, callable::walk_callable_type, class::walk_generic_alias, @@ -16,9 +16,9 @@ use crate::{ method::{walk_bound_method_type, walk_method_wrapper_type}, newtype::{NewType, walk_newtype_instance_type}, subclass_of::walk_subclass_of_type, - walk_bound_type_var_type, walk_intersection_type, walk_property_instance_type, - walk_type_alias_type, walk_type_var_type, walk_typed_dict_type, walk_typeguard_type, - walk_typeis_type, walk_union, + typevar::{TypeVarInstance, walk_bound_type_var_type, walk_type_var_type}, + walk_intersection_type, walk_property_instance_type, walk_type_alias_type, + walk_typed_dict_type, walk_typeguard_type, walk_typeis_type, walk_union, }, }; use std::cell::{Cell, RefCell}; From 38c273aa235aca9a4b2d9e229c9d596f9eab2c65 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 4 Mar 2026 17:23:58 +0000 Subject: [PATCH 197/261] [ty] Move tests and type-alias-related code out of `types.rs` (#23711) --- crates/ty_python_semantic/src/types.rs | 675 +----------------- .../ty_python_semantic/src/types/call/bind.rs | 17 +- .../ty_python_semantic/src/types/callable.rs | 9 +- crates/ty_python_semantic/src/types/class.rs | 13 +- .../ty_python_semantic/src/types/display.rs | 14 +- .../ty_python_semantic/src/types/function.rs | 11 +- .../ty_python_semantic/src/types/generics.rs | 4 +- .../src/types/infer/builder.rs | 20 +- crates/ty_python_semantic/src/types/method.rs | 7 +- crates/ty_python_semantic/src/types/mro.rs | 4 +- .../ty_python_semantic/src/types/relation.rs | 3 +- .../src/types/subclass_of.rs | 5 +- crates/ty_python_semantic/src/types/tests.rs | 345 +++++++++ .../src/types/type_alias.rs | 326 +++++++++ .../ty_python_semantic/src/types/visitor.rs | 6 +- 15 files changed, 744 insertions(+), 715 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/tests.rs create mode 100644 crates/ty_python_semantic/src/types/type_alias.rs diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f3fa233d9eedf..6aa64498291ee 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5,7 +5,6 @@ use rustc_hash::FxHashMap; use std::borrow::Cow; use std::cell::RefCell; -use std::fmt::Write; use std::time::Duration; use bitflags::bitflags; @@ -14,16 +13,14 @@ use context::InferContext; use ruff_db::Instant; use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; use ruff_db::files::File; -use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; use ruff_text_size::Ranged; use smallvec::smallvec_inline; use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; -pub(crate) use self::class::DynamicClassLiteral; pub use self::cyclic::CycleDetector; -pub(crate) use self::cyclic::{PairVisitor, TypeTransformer}; +pub(crate) use self::cyclic::TypeTransformer; pub(crate) use self::diagnostic::register_lints; pub use self::diagnostic::{TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_REFERENCE}; pub(crate) use self::infer::{ @@ -31,27 +28,27 @@ pub(crate) use self::infer::{ infer_expression_type, infer_expression_types, infer_scope_types, }; pub use self::known_instance::KnownInstanceType; +use self::set_theoretic::KnownUnion; pub(crate) use self::set_theoretic::builder::{IntersectionBuilder, UnionBuilder}; pub use self::set_theoretic::{ IntersectionType, NegativeIntersectionElements, NegativeIntersectionElementsIterator, UnionType, }; -use self::set_theoretic::{KnownUnion, walk_intersection_type, walk_union}; pub use self::signatures::ParameterKind; -pub(crate) use self::signatures::{CallableSignature, Signature}; +pub(crate) use self::signatures::Signature; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; use crate::place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, builtins_module_scope, imported_symbol, known_module_symbol, }; -use crate::semantic_index::definition::{Definition, DefinitionKind}; +use crate::semantic_index::definition::Definition; use crate::semantic_index::place::ScopedPlaceId; use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{imported_modules, place_table, semantic_index}; use crate::suppression::check_suppressions; use crate::types::bound_super::BoundSuperType; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; -pub(crate) use crate::types::callable::{CallableType, CallableTypeKind, CallableTypes}; +pub(crate) use crate::types::callable::{CallableType, CallableTypes}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; @@ -78,7 +75,8 @@ pub(crate) use crate::types::signatures::{Parameter, Parameters}; use crate::types::signatures::{ParameterForm, walk_signature}; use crate::types::special_form::TypeQualifier; use crate::types::tuple::TupleSpec; -pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; +use crate::types::type_alias::TypeAliasType; +pub(crate) use crate::types::typed_dict::TypedDictType; pub use crate::types::typevar::{ BindingContext, BoundTypeVarInstance, ParamSpecAttrKind, TypeVarBoundOrConstraints, TypeVarKind, }; @@ -130,7 +128,10 @@ mod signatures; mod special_form; mod string_annotation; mod subclass_of; +#[cfg(test)] +pub(crate) mod tests; mod tuple; +mod type_alias; mod typed_dict; mod typevar; mod unpacker; @@ -7023,314 +7024,6 @@ impl<'db> ModuleLiteralType<'db> { } } -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct PEP695TypeAliasType<'db> { - #[returns(ref)] - pub name: ast::name::Name, - - rhs_scope: ScopeId<'db>, - - specialization: Option>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for PEP695TypeAliasType<'_> {} - -fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - type_alias: PEP695TypeAliasType<'db>, - visitor: &V, -) { - visitor.visit_type(db, type_alias.value_type(db)); -} - -#[salsa::tracked] -impl<'db> PEP695TypeAliasType<'db> { - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - let scope = self.rhs_scope(db); - let type_alias_stmt_node = scope.node(db).expect_type_alias(); - semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) - } - - /// The RHS type of a PEP-695 style type alias with specialization applied. - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { - self.apply_function_specialization(db, self.raw_value_type(db)) - } - - /// The RHS type of a PEP-695 style type alias with *no* specialization applied. - /// Returns `Divergent` if the type alias is defined cyclically. - #[salsa::tracked( - cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { - value.cycle_normalized(db, *previous, cycle) - }, - heap_size=ruff_memory_usage::heap_size - )] - fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { - let scope = self.rhs_scope(db); - let module = parsed_module(db, scope.file(db)).load(db); - let type_alias_stmt_node = scope.node(db).expect_type_alias(); - let definition = self.definition(db); - - definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value) - } - - fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - if let Some(generic_context) = self.generic_context(db) { - let specialization = self - .specialization(db) - .unwrap_or_else(|| generic_context.default_specialization(db, None)); - ty.apply_specialization(db, specialization) - } else { - ty - } - } - - pub(crate) fn apply_specialization( - self, - db: &'db dyn Db, - f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, - ) -> PEP695TypeAliasType<'db> { - match self.generic_context(db) { - None => self, - - Some(generic_context) => { - // Note that at runtime, a specialized type alias is an instance of `typing.GenericAlias`. - // However, the `GenericAlias` type in ty is heavily special cased to refer to specialized - // class literals, so we instead represent specialized type aliases as instances of - // `typing.TypeAliasType` internally, and pass the specialization through to the value type, - // except when resolving to an instance of the type alias, or its display representation. - let specialization = f(generic_context); - PEP695TypeAliasType::new( - db, - self.name(db), - self.rhs_scope(db), - Some(specialization), - ) - } - } - } - - pub(crate) fn is_specialized(self, db: &'db dyn Db) -> bool { - self.specialization(db).is_some() - } - - #[salsa::tracked(cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - let scope = self.rhs_scope(db); - let file = scope.file(db); - let parsed = parsed_module(db, file).load(db); - let type_alias_stmt_node = scope.node(db).expect_type_alias(); - - type_alias_stmt_node - .node(&parsed) - .type_params - .as_ref() - .map(|type_params| { - let index = semantic_index(db, scope.file(db)); - let definition = index.expect_single_definition(type_alias_stmt_node); - GenericContext::from_type_params(db, index, definition, type_params) - }) - } -} - -/// A PEP 695 `types.TypeAliasType` created by manually calling the constructor. -/// -/// The value type is computed lazily via [`ManualPEP695TypeAliasType::value_type()`] -/// to avoid cycle non-convergence for mutually recursive definitions. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct ManualPEP695TypeAliasType<'db> { - #[returns(ref)] - pub name: ast::name::Name, - pub definition: Definition<'db>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for ManualPEP695TypeAliasType<'_> {} - -fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - type_alias: ManualPEP695TypeAliasType<'db>, - visitor: &V, -) { - visitor.visit_type(db, type_alias.value_type(db)); -} - -#[salsa::tracked] -impl<'db> ManualPEP695TypeAliasType<'db> { - /// The value type of this manual type alias. - /// - /// Computed lazily from the definition to avoid including the value in the interned - /// struct's identity. Returns `Divergent` if the type alias is defined cyclically. - #[salsa::tracked( - cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { - value.cycle_normalized(db, *previous, cycle) - }, - heap_size=ruff_memory_usage::heap_size - )] - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { - let definition = self.definition(db); - let file = definition.file(db); - let module = parsed_module(db, file).load(db); - let DefinitionKind::Assignment(assignment) = definition.kind(db) else { - return Type::unknown(); - }; - let value_node = assignment.value(&module); - let ast::Expr::Call(call) = value_node else { - return Type::unknown(); - }; - // The value is the second positional argument to TypeAliasType(name, value). - let Some(value_arg) = call.arguments.find_argument_value("value", 1) else { - return Type::unknown(); - }; - definition_expression_type(db, definition, value_arg) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub enum TypeAliasType<'db> { - /// A type alias defined using the PEP 695 `type` statement. - PEP695(PEP695TypeAliasType<'db>), - /// A type alias defined by manually instantiating the PEP 695 `types.TypeAliasType`. - ManualPEP695(ManualPEP695TypeAliasType<'db>), -} - -fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - type_alias: TypeAliasType<'db>, - visitor: &V, -) { - if !visitor.should_visit_lazy_type_attributes() { - return; - } - match type_alias { - TypeAliasType::PEP695(type_alias) => { - walk_pep_695_type_alias(db, type_alias, visitor); - } - TypeAliasType::ManualPEP695(type_alias) => { - walk_manual_pep_695_type_alias(db, type_alias, visitor); - } - } -} - -impl<'db> TypeAliasType<'db> { - pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { - match self { - TypeAliasType::PEP695(type_alias) => type_alias.name(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.name(db), - } - } - - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - match self { - TypeAliasType::PEP695(type_alias) => type_alias.definition(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.definition(db), - } - } - - pub fn value_type(self, db: &'db dyn Db) -> Type<'db> { - match self { - TypeAliasType::PEP695(type_alias) => type_alias.value_type(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), - } - } - - pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { - match self { - TypeAliasType::PEP695(type_alias) => type_alias.raw_value_type(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), - } - } - - pub(crate) fn as_pep_695_type_alias(self) -> Option> { - match self { - TypeAliasType::PEP695(type_alias) => Some(type_alias), - TypeAliasType::ManualPEP695(_) => None, - } - } - - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - // TODO: Add support for generic non-PEP695 type aliases. - match self { - TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db), - TypeAliasType::ManualPEP695(_) => None, - } - } - - pub(crate) fn specialization(self, db: &'db dyn Db) -> Option> { - match self { - TypeAliasType::PEP695(type_alias) => type_alias.specialization(db), - TypeAliasType::ManualPEP695(_) => None, - } - } - - fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - match self { - TypeAliasType::PEP695(type_alias) => type_alias.apply_function_specialization(db, ty), - TypeAliasType::ManualPEP695(_) => ty, - } - } - - pub(crate) fn apply_specialization( - self, - db: &'db dyn Db, - f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, - ) -> Self { - match self { - TypeAliasType::PEP695(type_alias) => { - TypeAliasType::PEP695(type_alias.apply_specialization(db, f)) - } - TypeAliasType::ManualPEP695(_) => self, - } - } - - /// Returns a struct that can display the fully qualified name of this type alias. - pub(crate) fn qualified_name(self, db: &'db dyn Db) -> QualifiedTypeAliasName<'db> { - QualifiedTypeAliasName::from_type_alias(db, self) - } -} - -// N.B. It would be incorrect to derive `Eq`, `PartialEq`, or `Hash` for this struct, -// because two `QualifiedTypeAliasName` instances might refer to different type aliases but -// have the same components. You'd expect them to compare equal, but they'd compare -// unequal if `PartialEq`/`Eq` were naively derived. -#[derive(Clone, Copy)] -pub(crate) struct QualifiedTypeAliasName<'db> { - db: &'db dyn Db, - type_alias: TypeAliasType<'db>, -} - -impl<'db> QualifiedTypeAliasName<'db> { - pub(crate) fn from_type_alias(db: &'db dyn Db, type_alias: TypeAliasType<'db>) -> Self { - Self { db, type_alias } - } - - /// Returns the components of the qualified name of this type alias, excluding the alias itself. - /// - /// For example, calling this method on a type alias `D` inside a class `C` in module `a.b` - /// would return `["a", "b", "C"]`. - pub(crate) fn components_excluding_self(&self) -> Vec { - let definition = self.type_alias.definition(self.db); - let file = definition.file(self.db); - let file_scope_id = definition.file_scope(self.db); - - // Type aliases are defined directly in their enclosing scope (no body scope like classes), - // so we don't skip any ancestor scopes. - display::qualified_name_components_from_scope(self.db, file, file_scope_id, 0) - } -} - -impl std::fmt::Display for QualifiedTypeAliasName<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for parent in self.components_excluding_self() { - f.write_str(&parent)?; - f.write_char('.')?; - } - f.write_str(self.type_alias.name(self.db)) - } -} - /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(super) struct MetaclassCandidate<'db> { @@ -7587,351 +7280,3 @@ impl EvaluationMode { #[cfg(not(debug_assertions))] #[cfg(target_pointer_width = "64")] static_assertions::assert_eq_size!(Type, [u8; 16]); - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::db::tests::{TestDbBuilder, setup_db}; - use crate::place::{typing_extensions_symbol, typing_symbol}; - use ruff_db::system::DbWithWritableSystem as _; - use ruff_python_ast::PythonVersion; - use test_case::test_case; - - /// Explicitly test for Python version <3.13 and >=3.13, to ensure that - /// the fallback to `typing_extensions` is working correctly. - /// See [`KnownClass::canonical_module`] for more information. - #[test_case(PythonVersion::PY312)] - #[test_case(PythonVersion::PY313)] - fn no_default_type_is_singleton(python_version: PythonVersion) { - let db = TestDbBuilder::new() - .with_python_version(python_version) - .build() - .unwrap(); - - let no_default = KnownClass::NoDefaultType.to_instance(&db); - - assert!(no_default.is_singleton(&db)); - } - - #[test] - fn typing_vs_typeshed_no_default() { - let db = TestDbBuilder::new() - .with_python_version(PythonVersion::PY313) - .build() - .unwrap(); - - let typing_no_default = typing_symbol(&db, "NoDefault").place.expect_type(); - let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault") - .place - .expect_type(); - - assert_eq!(typing_no_default.display(&db).to_string(), "NoDefault"); - assert_eq!( - typing_extensions_no_default.display(&db).to_string(), - "NoDefault" - ); - } - - /// All other tests also make sure that `Type::Todo` works as expected. This particular - /// test makes sure that we handle `Todo` types correctly, even if they originate from - /// different sources. - #[test] - fn todo_types() { - let db = setup_db(); - - let todo1 = todo_type!("1"); - let todo2 = todo_type!("2"); - - let int = KnownClass::Int.to_instance(&db); - - assert!(int.is_assignable_to(&db, todo1)); - - assert!(todo1.is_assignable_to(&db, int)); - - // We lose information when combining several `Todo` types. This is an - // acknowledged limitation of the current implementation. We cannot - // easily store the meta information of several `Todo`s in a single - // variant, as `TodoType` needs to implement `Copy`, meaning it can't - // contain `Vec`/`Box`/etc., and can't be boxed itself. - // - // Lifting this restriction would require us to intern `TodoType` in - // salsa, but that would mean we would have to pass in `db` everywhere. - - // A union of several `Todo` types collapses to a single `Todo` type: - assert!(UnionType::from_elements(&db, [todo1, todo2]).is_todo()); - - // And similar for intersection types: - assert!(IntersectionType::from_elements(&db, [todo1, todo2]).is_todo()); - assert!( - IntersectionBuilder::new(&db) - .add_positive(todo1) - .add_negative(todo2) - .build() - .is_todo() - ); - } - - #[test] - fn divergent_type() { - let db = setup_db(); - let div = Type::divergent(salsa::plumbing::Id::from_bits(1)); - - // The `Divergent` type must not be eliminated in union with other dynamic types, - // as this would prevent detection of divergent type inference using `Divergent`. - let union = UnionType::from_elements(&db, [Type::unknown(), div]); - assert_eq!(union.display(&db).to_string(), "Unknown | Divergent"); - - let union = UnionType::from_elements(&db, [div, Type::unknown()]); - assert_eq!(union.display(&db).to_string(), "Divergent | Unknown"); - - let union = UnionType::from_elements(&db, [div, Type::unknown(), todo_type!("1")]); - assert_eq!(union.display(&db).to_string(), "Divergent | Unknown"); - - assert!(div.is_equivalent_to(&db, div)); - assert!(!div.is_equivalent_to(&db, Type::unknown())); - assert!(!Type::unknown().is_equivalent_to(&db, div)); - assert!(!div.is_redundant_with(&db, Type::unknown())); - assert!(!Type::unknown().is_redundant_with(&db, div)); - - // `Divergent & T` and `Divergent & ~T` both simplify to `Divergent`, except for the - // specific case of `Divergent & Never`, which simplifies to `Never`. - let divergent_intersection = IntersectionBuilder::new(&db) - .add_positive(div) - .add_positive(todo_type!("2")) - .add_negative(todo_type!("3")) - .build(); - assert_eq!(divergent_intersection, div); - let divergent_intersection = IntersectionBuilder::new(&db) - .add_positive(todo_type!("2")) - .add_negative(todo_type!("3")) - .add_positive(div) - .build(); - assert_eq!(divergent_intersection, div); - let divergent_never_intersection = IntersectionBuilder::new(&db) - .add_positive(div) - .add_positive(Type::Never) - .build(); - assert_eq!(divergent_never_intersection, Type::Never); - let divergent_never_intersection = IntersectionBuilder::new(&db) - .add_positive(Type::Never) - .add_positive(div) - .build(); - assert_eq!(divergent_never_intersection, Type::Never); - - // The `object` type has a good convergence property, that is, its union with all other types is `object`. - // (e.g. `object | tuple[Divergent] == object`, `object | tuple[object] == object`) - // So we can safely eliminate `Divergent`. - let union = UnionType::from_elements(&db, [div, KnownClass::Object.to_instance(&db)]); - assert_eq!(union.display(&db).to_string(), "object"); - - let union = UnionType::from_elements(&db, [KnownClass::Object.to_instance(&db), div]); - assert_eq!(union.display(&db).to_string(), "object"); - - let recursive = UnionType::from_elements( - &db, - [ - KnownClass::List.to_specialized_instance(&db, &[div]), - Type::none(&db), - ], - ); - let nested_rec = KnownClass::List.to_specialized_instance(&db, &[recursive]); - assert_eq!( - nested_rec.display(&db).to_string(), - "list[list[Divergent] | None]" - ); - let normalized = nested_rec - .recursive_type_normalized_impl(&db, div, false) - .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "list[Divergent]"); - - let union = UnionType::from_elements(&db, [div, KnownClass::Int.to_instance(&db)]); - assert_eq!(union.display(&db).to_string(), "Divergent | int"); - let normalized = union - .recursive_type_normalized_impl(&db, div, false) - .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "int"); - - // The same can be said about intersections for the `Never` type. - let intersection = IntersectionType::from_elements(&db, [Type::Never, div]); - assert_eq!(intersection.display(&db).to_string(), "Never"); - - let intersection = IntersectionType::from_elements(&db, [div, Type::Never]); - assert_eq!(intersection.display(&db).to_string(), "Never"); - } - - #[test] - fn type_alias_variance() { - use crate::db::tests::TestDb; - use crate::place::global_symbol; - - fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> PEP695TypeAliasType<'db> { - let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); - let ty = global_symbol(db, module, name).place.expect_type(); - let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( - type_alias, - ))) = ty - else { - panic!("Expected `{name}` to be a type alias"); - }; - type_alias - } - fn get_bound_typevar<'db>( - db: &'db TestDb, - type_alias: PEP695TypeAliasType<'db>, - ) -> BoundTypeVarInstance<'db> { - let generic_context = type_alias.generic_context(db).unwrap(); - generic_context.variables(db).next().unwrap() - } - - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - r#" -class Covariant[T]: - def get(self) -> T: - raise ValueError - -class Contravariant[T]: - def set(self, value: T): - pass - -class Invariant[T]: - def get(self) -> T: - raise ValueError - def set(self, value: T): - pass - -class Bivariant[T]: - pass - -type CovariantAlias[T] = Covariant[T] -type ContravariantAlias[T] = Contravariant[T] -type InvariantAlias[T] = Invariant[T] -type BivariantAlias[T] = Bivariant[T] - -type RecursiveAlias[T] = None | list[RecursiveAlias[T]] -type RecursiveAlias2[T] = None | list[T] | list[RecursiveAlias2[T]] -"#, - ) - .unwrap(); - let covariant = get_type_alias(&db, "CovariantAlias"); - assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant)) - .variance_of(&db, get_bound_typevar(&db, covariant)), - TypeVarVariance::Covariant - ); - - let contravariant = get_type_alias(&db, "ContravariantAlias"); - assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant)) - .variance_of(&db, get_bound_typevar(&db, contravariant)), - TypeVarVariance::Contravariant - ); - - let invariant = get_type_alias(&db, "InvariantAlias"); - assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant)) - .variance_of(&db, get_bound_typevar(&db, invariant)), - TypeVarVariance::Invariant - ); - - let bivariant = get_type_alias(&db, "BivariantAlias"); - assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant)) - .variance_of(&db, get_bound_typevar(&db, bivariant)), - TypeVarVariance::Bivariant - ); - - let recursive = get_type_alias(&db, "RecursiveAlias"); - assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive)) - .variance_of(&db, get_bound_typevar(&db, recursive)), - TypeVarVariance::Bivariant - ); - - let recursive2 = get_type_alias(&db, "RecursiveAlias2"); - assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive2)) - .variance_of(&db, get_bound_typevar(&db, recursive2)), - TypeVarVariance::Invariant - ); - } - - #[test] - fn eager_expansion() { - use crate::db::tests::TestDb; - use crate::place::global_symbol; - - fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> Type<'db> { - let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); - let ty = global_symbol(db, module, name).place.expect_type(); - let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( - type_alias, - ))) = ty - else { - panic!("Expected `{name}` to be a type alias"); - }; - Type::TypeAlias(TypeAliasType::PEP695(type_alias)) - } - - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - r#" - -type IntStr = int | str -type ListIntStr = list[IntStr] -type RecursiveList[T] = list[T | RecursiveList[T]] -type RecursiveIntList = RecursiveList[int] -type Itself = Itself -type A = B -type B = A -type G[T] = H[T] -type H[T] = G[T] -"#, - ) - .unwrap(); - - let int_str = get_type_alias(&db, "IntStr"); - assert_eq!( - int_str.expand_eagerly(&db).display(&db).to_string(), - "int | str", - ); - - let list_int_str = get_type_alias(&db, "ListIntStr"); - assert_eq!( - list_int_str.expand_eagerly(&db).display(&db).to_string(), - "list[int | str]", - ); - - let rec_list = get_type_alias(&db, "RecursiveList"); - assert_eq!( - rec_list.expand_eagerly(&db).display(&db).to_string(), - "list[Divergent]", - ); - - let rec_int_list = get_type_alias(&db, "RecursiveIntList"); - assert_eq!( - rec_int_list.expand_eagerly(&db).display(&db).to_string(), - "list[Divergent]", - ); - - let itself = get_type_alias(&db, "Itself"); - assert_eq!( - itself.expand_eagerly(&db).display(&db).to_string(), - "Divergent", - ); - - let a = get_type_alias(&db, "A"); - assert_eq!(a.expand_eagerly(&db).display(&db).to_string(), "Divergent",); - - let b = get_type_alias(&db, "B"); - assert_eq!(b.expand_eagerly(&db).display(&db).to_string(), "Divergent",); - - let g = get_type_alias(&db, "G"); - assert_eq!(g.expand_eagerly(&db).display(&db).to_string(), "Divergent",); - - let h = get_type_alias(&db, "H"); - assert_eq!(h.expand_eagerly(&db).display(&db).to_string(), "Divergent",); - } -} diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 22c6017c03c70..15f55c7b0c61c 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -26,6 +26,7 @@ use crate::dunder_all::dunder_all_names; use crate::place::{DefinedPlace, Definedness, Place, known_module_symbol}; use crate::subscript::PyIndex; use crate::types::call::arguments::{Expansion, is_expandable_type}; +use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; use crate::types::diagnostic::{ CALL_NON_CALLABLE, CALL_TOP_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, @@ -42,16 +43,18 @@ use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError, }; use crate::types::known_instance::FieldInstance; -use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; +use crate::types::signatures::{ + CallableSignature, Parameter, ParameterForm, ParameterKind, Parameters, +}; use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::typevar::BoundTypeVarIdentity; use crate::types::{ - BoundMethodType, BoundTypeVarInstance, CallableSignature, CallableType, CallableTypeKind, - ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, EvaluationMode, GenericAlias, - InternedConstraintSet, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, - LiteralValueTypeKind, MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, - SpecialFormType, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, TypeVarVariance, - UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, + BoundMethodType, BoundTypeVarInstance, CallableType, ClassLiteral, DATACLASS_FLAGS, + DataclassFlags, DataclassParams, EvaluationMode, GenericAlias, InternedConstraintSet, + IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, LiteralValueTypeKind, + MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, TypeAliasType, + TypeContext, TypeVarBoundOrConstraints, TypeVarVariance, UnionBuilder, UnionType, + WrapperDescriptorKind, enums, list_members, }; use crate::{DisplaySettings, Program}; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index 99a4f7409c827..93e787e3395c9 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -6,13 +6,14 @@ use crate::{ place::Place, semantic_index::definition::Definition, types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableSignature, ClassType, - FindLegacyTypeVarsVisitor, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, - Parameter, Parameters, Signature, SubclassOfInner, Type, TypeContext, TypeMapping, - TypeVarBoundOrConstraints, UnionType, + ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, FindLegacyTypeVarsVisitor, + KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, Parameter, Parameters, + Signature, SubclassOfInner, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, + UnionType, constraints::{ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension}, generics::InferableTypeVars, relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}, + signatures::CallableSignature, visitor, walk_signature, }, }; diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 7d301c8ecfc00..49f0e80484bfd 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -17,6 +17,7 @@ use crate::semantic_index::{ DeclarationWithConstraint, SemanticIndex, attribute_declarations, attribute_scopes, }; use crate::types::bound_super::BoundSuperError; +use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; @@ -39,14 +40,14 @@ use crate::types::mro::DynamicMroError; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::tuple::{Tuple, TupleSpec, TupleType}; -use crate::types::typed_dict::typed_dict_params_from_class_def; +use crate::types::typed_dict::{TypedDictParams, typed_dict_params_from_class_def}; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ - ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType, - CallableTypeKind, CallableTypes, DATACLASS_FLAGS, DataclassFlags, DataclassParams, - FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownInstanceType, MaterializationKind, - PropertyInstanceType, TypeContext, TypeMapping, TypedDictParams, UnionBuilder, - VarianceInferable, binding_type, declaration_type, determine_upper_bound, + ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType, CallableTypes, + DATACLASS_FLAGS, DataclassFlags, DataclassParams, FindLegacyTypeVarsVisitor, + IntersectionBuilder, KnownInstanceType, MaterializationKind, PropertyInstanceType, TypeContext, + TypeMapping, UnionBuilder, VarianceInferable, binding_type, declaration_type, + determine_upper_bound, }; use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index d4d5716e17605..b6b0379a16d20 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -22,6 +22,7 @@ use crate::place::{DefinedPlace, Place}; use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::{FileScopeId, ScopeKind}; use crate::semantic_index::semantic_index; +use crate::types::callable::CallableTypeKind; use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::function::{FunctionType, OverloadLiteral}; use crate::types::generics::{GenericContext, Specialization}; @@ -32,11 +33,10 @@ use crate::types::tuple::TupleSpec; use crate::types::typevar::BoundTypeVarIdentity; use crate::types::visitor::TypeVisitor; use crate::types::{ - BindingContext, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType, - KnownClass, KnownInstanceType, LiteralValueType, LiteralValueTypeKind, MaterializationKind, - Protocol, ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, - SubclassOfType, Type, TypeAliasType, TypeGuardLike, TypedDictType, UnionType, - WrapperDescriptorKind, visitor, + BindingContext, CallableType, IntersectionType, KnownBoundMethodType, KnownClass, + KnownInstanceType, LiteralValueType, LiteralValueTypeKind, MaterializationKind, Protocol, + ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType, + Type, TypeAliasType, TypeGuardLike, TypedDictType, UnionType, WrapperDescriptorKind, visitor, }; /// A named item that can be either a class or a type alias. @@ -646,8 +646,8 @@ fn fmt_file_location<'db>( /// Returns the qualified name components for a scope, excluding the item itself. /// /// This is the shared logic used by both [`QualifiedClassName`](super::class::QualifiedClassName) -/// and [`QualifiedTypeAliasName`](super::QualifiedTypeAliasName) to compute the path components -/// (module, enclosing classes, functions) leading to an item. +/// and [`QualifiedTypeAliasName`](super::type_alias::QualifiedTypeAliasName) to compute the path +/// components (module, enclosing classes, functions) leading to an item. /// /// # Returns /// A vector of path components in order (e.g., `["module", "OuterClass", "InnerClass"]`) diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index d7246e915430e..4a1dc8ef700b7 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -65,6 +65,7 @@ use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{FileScopeId, SemanticIndex, semantic_index}; use crate::types::call::{Binding, CallArguments}; +use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; use crate::types::context::InferContext; use crate::types::diagnostic::{ @@ -85,11 +86,11 @@ use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelati use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; use crate::types::{ - ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, CallableTypeKind, - ClassBase, ClassLiteral, ClassType, DynamicType, FindLegacyTypeVarsVisitor, KnownClass, - KnownInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, - TypeContext, TypeMapping, TypeVarBoundOrConstraints, UnionBuilder, UnionType, binding_type, - definition_expression_type, infer_definition_types, walk_signature, + ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, + ClassLiteral, ClassType, DynamicType, FindLegacyTypeVarsVisitor, KnownClass, KnownInstanceType, + SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, TypeMapping, + TypeVarBoundOrConstraints, UnionBuilder, UnionType, binding_type, definition_expression_type, + infer_definition_types, walk_signature, }; use crate::{Db, FxOrderSet}; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 43687a23d2a8c..f02abde6ae80d 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -21,6 +21,7 @@ use crate::types::constraints::{ use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Parameters}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; +use crate::types::type_alias::{walk_manual_pep_695_type_alias, walk_pep_695_type_alias}; use crate::types::typevar::{ BoundTypeVarIdentity, TypeVarIdentity, TypeVarInstance, walk_type_var_bounds, }; @@ -30,8 +31,7 @@ use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypes, ClassLiteral, FindLegacyTypeVarsVisitor, IntersectionType, KnownClass, KnownInstanceType, MaterializationKind, Type, TypeAliasType, TypeContext, TypeMapping, TypeVarBoundOrConstraints, - TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_manual_pep_695_type_alias, - walk_pep_695_type_alias, + TypeVarKind, TypeVarVariance, UnionType, declaration_type, }; use crate::{Db, FxIndexMap, FxOrderMap, FxOrderSet}; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index dec757b567514..ac3652f42d199 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -61,6 +61,7 @@ use crate::semantic_index::{ }; use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind}; +use crate::types::callable::CallableTypeKind; use crate::types::class::{ AbstractMethod, ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, FieldKind, @@ -122,6 +123,7 @@ use crate::types::newtype::NewType; use crate::types::set_theoretic::RecursivelyDefined; use crate::types::subclass_of::SubclassOfInner; use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType}; +use crate::types::type_alias::{ManualPEP695TypeAliasType, PEP695TypeAliasType}; use crate::types::typed_dict::{ TypedDictAssignmentKind, TypedDictKeyAssignment, validate_typed_dict_constructor, validate_typed_dict_dict_literal, @@ -132,15 +134,15 @@ use crate::types::typevar::{ }; use crate::types::visitor::find_over_type; use crate::types::{ - CallDunderError, CallableBinding, CallableType, CallableTypeKind, ClassType, DataclassParams, - DynamicType, EvaluationMode, GenericAlias, InternedConstraintSet, InternedType, - IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, - LintDiagnosticGuard, LiteralValueTypeKind, ManualPEP695TypeAliasType, MemberLookupPolicy, - MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, - Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, - TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, binding_type, - definition_expression_type, infer_complete_scope_types, infer_scope_types, todo_type, + CallDunderError, CallableBinding, CallableType, ClassType, DataclassParams, DynamicType, + EvaluationMode, GenericAlias, InternedConstraintSet, InternedType, IntersectionBuilder, + IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, + LiteralValueTypeKind, MemberLookupPolicy, MetaclassCandidate, ParamSpecAttrKind, Parameter, + ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, + Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, + TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, + UnionType, binding_type, definition_expression_type, infer_complete_scope_types, + infer_scope_types, todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index 99510ecab080c..e2d7462194142 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -4,14 +4,15 @@ use ruff_python_ast::name::Name; use crate::{ Db, types::{ - CallableSignature, CallableType, CallableTypeKind, KnownClass, LiteralValueType, - LiteralValueTypeKind, Parameter, Parameters, PropertyInstanceType, Signature, - StringLiteralType, Type, UnionType, + CallableType, KnownClass, LiteralValueType, LiteralValueTypeKind, Parameter, Parameters, + PropertyInstanceType, Signature, StringLiteralType, Type, UnionType, + callable::CallableTypeKind, constraints::{ConstraintSet, ConstraintSetBuilder}, function::FunctionType, generics::InferableTypeVars, known_instance::InternedConstraintSet, relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}, + signatures::CallableSignature, visitor, }, }; diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index c8ee82e4a411e..5fa1e766a6959 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -5,11 +5,11 @@ use indexmap::IndexMap; use rustc_hash::{FxBuildHasher, FxHashSet}; use crate::Db; +use crate::types::class::DynamicClassLiteral; use crate::types::class_base::ClassBase; use crate::types::generics::Specialization; use crate::types::{ - ClassLiteral, ClassType, DynamicClassLiteral, KnownInstanceType, SpecialFormType, - StaticClassLiteral, Type, + ClassLiteral, ClassType, KnownInstanceType, SpecialFormType, StaticClassLiteral, Type, }; use itertools::Itertools; diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 952520b27fe21..27225620f98a0 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -6,11 +6,12 @@ use crate::place::{DefinedPlace, Place}; use crate::types::constraints::{ ConstraintSetBuilder, IteratorConstraintsExtension, OptionConstraintsExtension, }; +use crate::types::cyclic::PairVisitor; use crate::types::enums::is_single_member_enum; use crate::types::set_theoretic::RecursivelyDefined; use crate::types::{ CallableType, ClassBase, ClassType, CycleDetector, DynamicType, KnownClass, KnownInstanceType, - LiteralValueTypeKind, MemberLookupPolicy, PairVisitor, ProtocolInstanceType, SubclassOfInner, + LiteralValueTypeKind, MemberLookupPolicy, ProtocolInstanceType, SubclassOfInner, TypeVarBoundOrConstraints, UnionType, }; use crate::{ diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 6e244ebb8233b..b86c6a9e8c8d3 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,13 +1,14 @@ use crate::place::PlaceAndQualifiers; use crate::semantic_index::definition::Definition; +use crate::types::class::DynamicClassLiteral; use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; use crate::types::generics::InferableTypeVars; use crate::types::protocol_class::ProtocolClass; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::variance::VarianceInferable; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, ClassType, DynamicClassLiteral, - DynamicType, FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, + ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, ClassType, DynamicType, + FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, SpecialFormType, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, TypeVarVariance, TypedDictType, UnionType, todo_type, }; diff --git a/crates/ty_python_semantic/src/types/tests.rs b/crates/ty_python_semantic/src/types/tests.rs new file mode 100644 index 0000000000000..700f1045f32df --- /dev/null +++ b/crates/ty_python_semantic/src/types/tests.rs @@ -0,0 +1,345 @@ +use super::*; +use crate::db::tests::{TestDbBuilder, setup_db}; +use crate::place::{typing_extensions_symbol, typing_symbol}; +use crate::types::type_alias::PEP695TypeAliasType; +use ruff_db::system::DbWithWritableSystem as _; +use ruff_python_ast::PythonVersion; +use test_case::test_case; + +/// Explicitly test for Python version <3.13 and >=3.13, to ensure that +/// the fallback to `typing_extensions` is working correctly. +/// See [`KnownClass::canonical_module`] for more information. +#[test_case(PythonVersion::PY312)] +#[test_case(PythonVersion::PY313)] +fn no_default_type_is_singleton(python_version: PythonVersion) { + let db = TestDbBuilder::new() + .with_python_version(python_version) + .build() + .unwrap(); + + let no_default = KnownClass::NoDefaultType.to_instance(&db); + + assert!(no_default.is_singleton(&db)); +} + +#[test] +fn typing_vs_typeshed_no_default() { + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::PY313) + .build() + .unwrap(); + + let typing_no_default = typing_symbol(&db, "NoDefault").place.expect_type(); + let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault") + .place + .expect_type(); + + assert_eq!(typing_no_default.display(&db).to_string(), "NoDefault"); + assert_eq!( + typing_extensions_no_default.display(&db).to_string(), + "NoDefault" + ); +} + +/// All other tests also make sure that `Type::Todo` works as expected. This particular +/// test makes sure that we handle `Todo` types correctly, even if they originate from +/// different sources. +#[test] +fn todo_types() { + let db = setup_db(); + + let todo1 = todo_type!("1"); + let todo2 = todo_type!("2"); + + let int = KnownClass::Int.to_instance(&db); + + assert!(int.is_assignable_to(&db, todo1)); + + assert!(todo1.is_assignable_to(&db, int)); + + // We lose information when combining several `Todo` types. This is an + // acknowledged limitation of the current implementation. We cannot + // easily store the meta information of several `Todo`s in a single + // variant, as `TodoType` needs to implement `Copy`, meaning it can't + // contain `Vec`/`Box`/etc., and can't be boxed itself. + // + // Lifting this restriction would require us to intern `TodoType` in + // salsa, but that would mean we would have to pass in `db` everywhere. + + // A union of several `Todo` types collapses to a single `Todo` type: + assert!(UnionType::from_elements(&db, [todo1, todo2]).is_todo()); + + // And similar for intersection types: + assert!(IntersectionType::from_elements(&db, [todo1, todo2]).is_todo()); + assert!( + IntersectionBuilder::new(&db) + .add_positive(todo1) + .add_negative(todo2) + .build() + .is_todo() + ); +} + +#[test] +fn divergent_type() { + let db = setup_db(); + let div = Type::divergent(salsa::plumbing::Id::from_bits(1)); + + // The `Divergent` type must not be eliminated in union with other dynamic types, + // as this would prevent detection of divergent type inference using `Divergent`. + let union = UnionType::from_elements(&db, [Type::unknown(), div]); + assert_eq!(union.display(&db).to_string(), "Unknown | Divergent"); + + let union = UnionType::from_elements(&db, [div, Type::unknown()]); + assert_eq!(union.display(&db).to_string(), "Divergent | Unknown"); + + let union = UnionType::from_elements(&db, [div, Type::unknown(), todo_type!("1")]); + assert_eq!(union.display(&db).to_string(), "Divergent | Unknown"); + + assert!(div.is_equivalent_to(&db, div)); + assert!(!div.is_equivalent_to(&db, Type::unknown())); + assert!(!Type::unknown().is_equivalent_to(&db, div)); + assert!(!div.is_redundant_with(&db, Type::unknown())); + assert!(!Type::unknown().is_redundant_with(&db, div)); + + // `Divergent & T` and `Divergent & ~T` both simplify to `Divergent`, except for the + // specific case of `Divergent & Never`, which simplifies to `Never`. + let divergent_intersection = IntersectionBuilder::new(&db) + .add_positive(div) + .add_positive(todo_type!("2")) + .add_negative(todo_type!("3")) + .build(); + assert_eq!(divergent_intersection, div); + let divergent_intersection = IntersectionBuilder::new(&db) + .add_positive(todo_type!("2")) + .add_negative(todo_type!("3")) + .add_positive(div) + .build(); + assert_eq!(divergent_intersection, div); + let divergent_never_intersection = IntersectionBuilder::new(&db) + .add_positive(div) + .add_positive(Type::Never) + .build(); + assert_eq!(divergent_never_intersection, Type::Never); + let divergent_never_intersection = IntersectionBuilder::new(&db) + .add_positive(Type::Never) + .add_positive(div) + .build(); + assert_eq!(divergent_never_intersection, Type::Never); + + // The `object` type has a good convergence property, that is, its union with all other types is `object`. + // (e.g. `object | tuple[Divergent] == object`, `object | tuple[object] == object`) + // So we can safely eliminate `Divergent`. + let union = UnionType::from_elements(&db, [div, KnownClass::Object.to_instance(&db)]); + assert_eq!(union.display(&db).to_string(), "object"); + + let union = UnionType::from_elements(&db, [KnownClass::Object.to_instance(&db), div]); + assert_eq!(union.display(&db).to_string(), "object"); + + let recursive = UnionType::from_elements( + &db, + [ + KnownClass::List.to_specialized_instance(&db, &[div]), + Type::none(&db), + ], + ); + let nested_rec = KnownClass::List.to_specialized_instance(&db, &[recursive]); + assert_eq!( + nested_rec.display(&db).to_string(), + "list[list[Divergent] | None]" + ); + let normalized = nested_rec + .recursive_type_normalized_impl(&db, div, false) + .unwrap(); + assert_eq!(normalized.display(&db).to_string(), "list[Divergent]"); + + let union = UnionType::from_elements(&db, [div, KnownClass::Int.to_instance(&db)]); + assert_eq!(union.display(&db).to_string(), "Divergent | int"); + let normalized = union + .recursive_type_normalized_impl(&db, div, false) + .unwrap(); + assert_eq!(normalized.display(&db).to_string(), "int"); + + // The same can be said about intersections for the `Never` type. + let intersection = IntersectionType::from_elements(&db, [Type::Never, div]); + assert_eq!(intersection.display(&db).to_string(), "Never"); + + let intersection = IntersectionType::from_elements(&db, [div, Type::Never]); + assert_eq!(intersection.display(&db).to_string(), "Never"); +} + +#[test] +fn type_alias_variance() { + use crate::db::tests::TestDb; + use crate::place::global_symbol; + + fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> PEP695TypeAliasType<'db> { + let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); + let ty = global_symbol(db, module, name).place.expect_type(); + let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( + type_alias, + ))) = ty + else { + panic!("Expected `{name}` to be a type alias"); + }; + type_alias + } + fn get_bound_typevar<'db>( + db: &'db TestDb, + type_alias: PEP695TypeAliasType<'db>, + ) -> BoundTypeVarInstance<'db> { + let generic_context = type_alias.generic_context(db).unwrap(); + generic_context.variables(db).next().unwrap() + } + + let mut db = setup_db(); + db.write_dedented( + "/src/a.py", + r#" +class Covariant[T]: + def get(self) -> T: + raise ValueError + +class Contravariant[T]: + def set(self, value: T): + pass + +class Invariant[T]: + def get(self) -> T: + raise ValueError + def set(self, value: T): + pass + +class Bivariant[T]: + pass + +type CovariantAlias[T] = Covariant[T] +type ContravariantAlias[T] = Contravariant[T] +type InvariantAlias[T] = Invariant[T] +type BivariantAlias[T] = Bivariant[T] + +type RecursiveAlias[T] = None | list[RecursiveAlias[T]] +type RecursiveAlias2[T] = None | list[T] | list[RecursiveAlias2[T]] +"#, + ) + .unwrap(); + let covariant = get_type_alias(&db, "CovariantAlias"); + assert_eq!( + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant)) + .variance_of(&db, get_bound_typevar(&db, covariant)), + TypeVarVariance::Covariant + ); + + let contravariant = get_type_alias(&db, "ContravariantAlias"); + assert_eq!( + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant)) + .variance_of(&db, get_bound_typevar(&db, contravariant)), + TypeVarVariance::Contravariant + ); + + let invariant = get_type_alias(&db, "InvariantAlias"); + assert_eq!( + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant)) + .variance_of(&db, get_bound_typevar(&db, invariant)), + TypeVarVariance::Invariant + ); + + let bivariant = get_type_alias(&db, "BivariantAlias"); + assert_eq!( + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant)) + .variance_of(&db, get_bound_typevar(&db, bivariant)), + TypeVarVariance::Bivariant + ); + + let recursive = get_type_alias(&db, "RecursiveAlias"); + assert_eq!( + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive)) + .variance_of(&db, get_bound_typevar(&db, recursive)), + TypeVarVariance::Bivariant + ); + + let recursive2 = get_type_alias(&db, "RecursiveAlias2"); + assert_eq!( + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive2)) + .variance_of(&db, get_bound_typevar(&db, recursive2)), + TypeVarVariance::Invariant + ); +} + +#[test] +fn eager_expansion() { + use crate::db::tests::TestDb; + use crate::place::global_symbol; + + fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> Type<'db> { + let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); + let ty = global_symbol(db, module, name).place.expect_type(); + let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( + type_alias, + ))) = ty + else { + panic!("Expected `{name}` to be a type alias"); + }; + Type::TypeAlias(TypeAliasType::PEP695(type_alias)) + } + + let mut db = setup_db(); + db.write_dedented( + "/src/a.py", + r#" + +type IntStr = int | str +type ListIntStr = list[IntStr] +type RecursiveList[T] = list[T | RecursiveList[T]] +type RecursiveIntList = RecursiveList[int] +type Itself = Itself +type A = B +type B = A +type G[T] = H[T] +type H[T] = G[T] +"#, + ) + .unwrap(); + + let int_str = get_type_alias(&db, "IntStr"); + assert_eq!( + int_str.expand_eagerly(&db).display(&db).to_string(), + "int | str", + ); + + let list_int_str = get_type_alias(&db, "ListIntStr"); + assert_eq!( + list_int_str.expand_eagerly(&db).display(&db).to_string(), + "list[int | str]", + ); + + let rec_list = get_type_alias(&db, "RecursiveList"); + assert_eq!( + rec_list.expand_eagerly(&db).display(&db).to_string(), + "list[Divergent]", + ); + + let rec_int_list = get_type_alias(&db, "RecursiveIntList"); + assert_eq!( + rec_int_list.expand_eagerly(&db).display(&db).to_string(), + "list[Divergent]", + ); + + let itself = get_type_alias(&db, "Itself"); + assert_eq!( + itself.expand_eagerly(&db).display(&db).to_string(), + "Divergent", + ); + + let a = get_type_alias(&db, "A"); + assert_eq!(a.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + + let b = get_type_alias(&db, "B"); + assert_eq!(b.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + + let g = get_type_alias(&db, "G"); + assert_eq!(g.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + + let h = get_type_alias(&db, "H"); + assert_eq!(h.expand_eagerly(&db).display(&db).to_string(), "Divergent",); +} diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs new file mode 100644 index 0000000000000..d8203f680e3db --- /dev/null +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -0,0 +1,326 @@ +use std::fmt::Write; + +use crate::{ + Db, + semantic_index::{ + definition::{Definition, DefinitionKind}, + scope::ScopeId, + semantic_index, + }, + types::{ + GenericContext, Type, definition_expression_type, + display::qualified_name_components_from_scope, generics::Specialization, visitor, + }, +}; + +use ruff_db::parsed::parsed_module; +use ruff_python_ast as ast; +use ruff_python_ast::name::Name; + +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct PEP695TypeAliasType<'db> { + #[returns(ref)] + pub name: Name, + + rhs_scope: ScopeId<'db>, + + pub(super) specialization: Option>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for PEP695TypeAliasType<'_> {} + +pub(super) fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + type_alias: PEP695TypeAliasType<'db>, + visitor: &V, +) { + visitor.visit_type(db, type_alias.value_type(db)); +} + +#[salsa::tracked] +impl<'db> PEP695TypeAliasType<'db> { + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + let scope = self.rhs_scope(db); + let type_alias_stmt_node = scope.node(db).expect_type_alias(); + semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) + } + + /// The RHS type of a PEP-695 style type alias with specialization applied. + pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + self.apply_function_specialization(db, self.raw_value_type(db)) + } + + /// The RHS type of a PEP-695 style type alias with *no* specialization applied. + /// Returns `Divergent` if the type alias is defined cyclically. + #[salsa::tracked( + cycle_initial=|_, id, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { + value.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { + let scope = self.rhs_scope(db); + let module = parsed_module(db, scope.file(db)).load(db); + let type_alias_stmt_node = scope.node(db).expect_type_alias(); + let definition = self.definition(db); + + definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value) + } + + fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + if let Some(generic_context) = self.generic_context(db) { + let specialization = self + .specialization(db) + .unwrap_or_else(|| generic_context.default_specialization(db, None)); + ty.apply_specialization(db, specialization) + } else { + ty + } + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> PEP695TypeAliasType<'db> { + match self.generic_context(db) { + None => self, + + Some(generic_context) => { + // Note that at runtime, a specialized type alias is an instance of `typing.GenericAlias`. + // However, the `GenericAlias` type in ty is heavily special cased to refer to specialized + // class literals, so we instead represent specialized type aliases as instances of + // `typing.TypeAliasType` internally, and pass the specialization through to the value type, + // except when resolving to an instance of the type alias, or its display representation. + let specialization = f(generic_context); + PEP695TypeAliasType::new( + db, + self.name(db), + self.rhs_scope(db), + Some(specialization), + ) + } + } + } + + pub(crate) fn is_specialized(self, db: &'db dyn Db) -> bool { + self.specialization(db).is_some() + } + + #[salsa::tracked(cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + let scope = self.rhs_scope(db); + let file = scope.file(db); + let parsed = parsed_module(db, file).load(db); + let type_alias_stmt_node = scope.node(db).expect_type_alias(); + + type_alias_stmt_node + .node(&parsed) + .type_params + .as_ref() + .map(|type_params| { + let index = semantic_index(db, scope.file(db)); + let definition = index.expect_single_definition(type_alias_stmt_node); + GenericContext::from_type_params(db, index, definition, type_params) + }) + } +} + +/// A PEP 695 `types.TypeAliasType` created by manually calling the constructor. +/// +/// The value type is computed lazily via [`ManualPEP695TypeAliasType::value_type()`] +/// to avoid cycle non-convergence for mutually recursive definitions. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct ManualPEP695TypeAliasType<'db> { + #[returns(ref)] + pub name: Name, + pub definition: Definition<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for ManualPEP695TypeAliasType<'_> {} + +pub(super) fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + type_alias: ManualPEP695TypeAliasType<'db>, + visitor: &V, +) { + visitor.visit_type(db, type_alias.value_type(db)); +} + +#[salsa::tracked] +impl<'db> ManualPEP695TypeAliasType<'db> { + /// The value type of this manual type alias. + /// + /// Computed lazily from the definition to avoid including the value in the interned + /// struct's identity. Returns `Divergent` if the type alias is defined cyclically. + #[salsa::tracked( + cycle_initial=|_, id, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { + value.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + let definition = self.definition(db); + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + return Type::unknown(); + }; + let value_node = assignment.value(&module); + let ast::Expr::Call(call) = value_node else { + return Type::unknown(); + }; + // The value is the second positional argument to TypeAliasType(name, value). + let Some(value_arg) = call.arguments.find_argument_value("value", 1) else { + return Type::unknown(); + }; + definition_expression_type(db, definition, value_arg) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub enum TypeAliasType<'db> { + /// A type alias defined using the PEP 695 `type` statement. + PEP695(PEP695TypeAliasType<'db>), + /// A type alias defined by manually instantiating the PEP 695 `types.TypeAliasType`. + ManualPEP695(ManualPEP695TypeAliasType<'db>), +} + +pub(super) fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + type_alias: TypeAliasType<'db>, + visitor: &V, +) { + if !visitor.should_visit_lazy_type_attributes() { + return; + } + match type_alias { + TypeAliasType::PEP695(type_alias) => { + walk_pep_695_type_alias(db, type_alias, visitor); + } + TypeAliasType::ManualPEP695(type_alias) => { + walk_manual_pep_695_type_alias(db, type_alias, visitor); + } + } +} + +impl<'db> TypeAliasType<'db> { + pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.name(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.name(db), + } + } + + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.definition(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.definition(db), + } + } + + pub fn value_type(self, db: &'db dyn Db) -> Type<'db> { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.value_type(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), + } + } + + pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.raw_value_type(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), + } + } + + pub(crate) fn as_pep_695_type_alias(self) -> Option> { + match self { + TypeAliasType::PEP695(type_alias) => Some(type_alias), + TypeAliasType::ManualPEP695(_) => None, + } + } + + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + // TODO: Add support for generic non-PEP695 type aliases. + match self { + TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db), + TypeAliasType::ManualPEP695(_) => None, + } + } + + pub(crate) fn specialization(self, db: &'db dyn Db) -> Option> { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.specialization(db), + TypeAliasType::ManualPEP695(_) => None, + } + } + + pub(super) fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.apply_function_specialization(db, ty), + TypeAliasType::ManualPEP695(_) => ty, + } + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> Self { + match self { + TypeAliasType::PEP695(type_alias) => { + TypeAliasType::PEP695(type_alias.apply_specialization(db, f)) + } + TypeAliasType::ManualPEP695(_) => self, + } + } + + /// Returns a struct that can display the fully qualified name of this type alias. + pub(crate) fn qualified_name(self, db: &'db dyn Db) -> QualifiedTypeAliasName<'db> { + QualifiedTypeAliasName::from_type_alias(db, self) + } +} + +// N.B. It would be incorrect to derive `Eq`, `PartialEq`, or `Hash` for this struct, +// because two `QualifiedTypeAliasName` instances might refer to different type aliases but +// have the same components. You'd expect them to compare equal, but they'd compare +// unequal if `PartialEq`/`Eq` were naively derived. +#[derive(Clone, Copy)] +pub(crate) struct QualifiedTypeAliasName<'db> { + db: &'db dyn Db, + type_alias: TypeAliasType<'db>, +} + +impl<'db> QualifiedTypeAliasName<'db> { + pub(crate) fn from_type_alias(db: &'db dyn Db, type_alias: TypeAliasType<'db>) -> Self { + Self { db, type_alias } + } + + /// Returns the components of the qualified name of this type alias, excluding the alias itself. + /// + /// For example, calling this method on a type alias `D` inside a class `C` in module `a.b` + /// would return `["a", "b", "C"]`. + pub(crate) fn components_excluding_self(&self) -> Vec { + let definition = self.type_alias.definition(self.db); + let file = definition.file(self.db); + let file_scope_id = definition.file_scope(self.db); + + // Type aliases are defined directly in their enclosing scope (no body scope like classes), + // so we don't skip any ancestor scopes. + qualified_name_components_from_scope(self.db, file, file_scope_id, 0) + } +} + +impl std::fmt::Display for QualifiedTypeAliasName<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for parent in self.components_excluding_self() { + f.write_str(&parent)?; + f.write_char('.')?; + } + f.write_str(self.type_alias.name(self.db)) + } +} diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index d457daccac452..ea9cb64b21348 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -15,10 +15,12 @@ use crate::{ known_instance::walk_known_instance_type, method::{walk_bound_method_type, walk_method_wrapper_type}, newtype::{NewType, walk_newtype_instance_type}, + set_theoretic::{walk_intersection_type, walk_union}, subclass_of::walk_subclass_of_type, + type_alias::walk_type_alias_type, + typed_dict::walk_typed_dict_type, typevar::{TypeVarInstance, walk_bound_type_var_type, walk_type_var_type}, - walk_intersection_type, walk_property_instance_type, walk_type_alias_type, - walk_typed_dict_type, walk_typeguard_type, walk_typeis_type, walk_union, + walk_property_instance_type, walk_typeguard_type, walk_typeis_type, }, }; use std::cell::{Cell, RefCell}; From 149c5786ca6ae135431b86d1cd8ab74763b9a95a Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 4 Mar 2026 13:10:09 -0500 Subject: [PATCH 198/261] [ty] Rework module resolution to be breadth-first instead of depth-first (#22449) ## Summary By making the algorithm breadth-first/incremental, the logic has a coherent translation to computing all_modules. Thus this is ground-work for auto-complete and auto-import adding various missing import semantics (extremely optimistically, we could actually make it share the same code!). In addition, this fixes a few corner-case issues with our module resolution: * A regular package (or module) in a later search-path now properly shadows a namespace package in an earlier one, which matches runtime behaviour. * We now consider all stub-packages to have higher priority than non-stub-packages, independent of search-path ordering. In most cases this means our behaviour will now be "search all paths for stubs, then search all paths for implementations." Co-authored-by: Andrew Gallant --- crates/ty_module_resolver/src/list.rs | 66 +- crates/ty_module_resolver/src/path.rs | 4 + crates/ty_module_resolver/src/resolve.rs | 659 ++++++++---------- .../resources/mdtest/import/namespace.md | 67 ++ .../resources/mdtest/import/stub_packages.md | 123 +++- 5 files changed, 520 insertions(+), 399 deletions(-) diff --git a/crates/ty_module_resolver/src/list.rs b/crates/ty_module_resolver/src/list.rs index 1770d2a5367cf..7f40a39924d69 100644 --- a/crates/ty_module_resolver/src/list.rs +++ b/crates/ty_module_resolver/src/list.rs @@ -25,28 +25,40 @@ pub fn all_modules(db: &dyn Db) -> Vec> { /// List all available top-level modules. #[salsa::tracked] pub fn list_modules(db: &dyn Db) -> Vec> { - let mut modules = BTreeMap::new(); + let mut modules: BTreeMap<&ModuleName, ListedModule<'_>> = BTreeMap::new(); for search_path in search_paths(db, ModuleResolveMode::StubsAllowed) { - for module in list_modules_in(db, SearchPathIngredient::new(db, search_path.clone())) { - match modules.entry(module.name(db)) { + for new in list_modules_in(db, SearchPathIngredient::new(db, search_path.clone())) { + match modules.entry(new.module(db).name(db)) { Entry::Vacant(entry) => { - entry.insert(module); + entry.insert(new); } Entry::Occupied(mut entry) => { - // The only case where a module can override - // a module with the same name in a higher - // precedent search path is if the higher - // precedent search path contained a namespace - // package and the lower precedent search path - // contained a "regular" module. - if let (None, Some(_)) = (entry.get().search_path(db), module.search_path(db)) { - entry.insert(module); + // A module can override a module with the same name in + // a higher precedent search path when either of the following + // are true: + // + // 1. The higher precedent search path contained a namespace + // package and the lower precedent search path contained + // a "regular" module/package. + // 2. The new module is from a stub package (`foo-stubs`), + // which has priority regardless of search path ordering + // per the typing spec's import resolution ordering. + let existing = entry.get(); + let existing_is_namespace = existing.module(db).search_path(db).is_none(); + let new_is_non_namespace = new.module(db).search_path(db).is_some(); + if (existing_is_namespace && new_is_non_namespace) + || (!existing.is_stub_package(db) && new.is_stub_package(db)) + { + entry.insert(new); } } } } } - modules.into_values().collect() + modules + .into_values() + .map(|listed| listed.module(db)) + .collect() } #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] @@ -60,7 +72,7 @@ struct SearchPathIngredient<'db> { fn list_modules_in<'db>( db: &'db dyn Db, search_path: SearchPathIngredient<'db>, -) -> Vec> { +) -> Vec> { tracing::debug!("Listing modules in search path '{}'", search_path.path(db)); let mut lister = Lister::new(db, search_path.path(db)); match search_path.path(db).as_path() { @@ -89,6 +101,15 @@ fn list_modules_in<'db>( lister.into_modules() } +/// A module paired with whether it came from a stub package. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +struct ListedModule<'db> { + module: Module<'db>, + is_stub_package: bool, +} + +impl get_size2::GetSize for ListedModule<'_> {} + /// An implementation helper for "list all modules." /// /// This is responsible for accumulating modules indexed by @@ -99,7 +120,7 @@ fn list_modules_in<'db>( struct Lister<'db> { db: &'db dyn Db, search_path: &'db SearchPath, - modules: BTreeMap<&'db ModuleName, Module<'db>>, + modules: BTreeMap<&'db ModuleName, ListedModule<'db>>, } impl<'db> Lister<'db> { @@ -114,7 +135,7 @@ impl<'db> Lister<'db> { } /// Returns the modules collected, sorted by module name. - fn into_modules(self) -> Vec> { + fn into_modules(self) -> Vec> { self.modules.into_values().collect() } @@ -246,22 +267,23 @@ impl<'db> Lister<'db> { /// existing entry, then this is a no-op. That is, this assumes that the /// caller looks for modules in search path priority order. fn add_module(&mut self, path: &ModulePath, module: Module<'db>) { + let listed = ListedModule::new(self.db, module, path.is_stub_package()); let mut entry = match self.modules.entry(module.name(self.db)) { Entry::Vacant(entry) => { - entry.insert(module); + entry.insert(listed); return; } Entry::Occupied(entry) => entry, }; - let existing = entry.get(); + let existing = entry.get().module(self.db); match (existing.search_path(self.db), module.search_path(self.db)) { // When we had a namespace package and now try to // insert a non-namespace package, the latter always // takes precedent, even if it's in a lower priority // search path. (None, Some(_)) => { - entry.insert(module); + entry.insert(listed); } (Some(_), Some(_)) => { // Merging across search paths is only necessary for @@ -278,7 +300,7 @@ impl<'db> Lister<'db> { if existing.kind(self.db) == ModuleKind::Module && module.kind(self.db) == ModuleKind::Package { - entry.insert(module); + entry.insert(listed); return; } // Or if we have two file modules and the new one @@ -287,13 +309,13 @@ impl<'db> Lister<'db> { && module.kind(self.db) == ModuleKind::Module && path.is_stub_file() { - entry.insert(module); + entry.insert(listed); return; } // Or... if we have a stub package, the stub package // always gets priority. if path.is_stub_package() { - entry.insert(module); + entry.insert(listed); } } _ => {} diff --git a/crates/ty_module_resolver/src/path.rs b/crates/ty_module_resolver/src/path.rs index ef6ca48b58b60..e5b0ff3378271 100644 --- a/crates/ty_module_resolver/src/path.rs +++ b/crates/ty_module_resolver/src/path.rs @@ -325,6 +325,10 @@ impl ModulePath { relative_path: relative_path.with_extension("py"), }) } + + pub(crate) fn into_search_path(self) -> SearchPath { + self.search_path + } } impl PartialEq for ModulePath { diff --git a/crates/ty_module_resolver/src/resolve.rs b/crates/ty_module_resolver/src/resolve.rs index 0532222577d47..31e00f19c01fe 100644 --- a/crates/ty_module_resolver/src/resolve.rs +++ b/crates/ty_module_resolver/src/resolve.rs @@ -32,11 +32,8 @@ specifies ty's implementation of Python's import resolution algorithm. */ use std::borrow::Cow; -use std::fmt; use std::iter::FusedIterator; -use std::str::Split; -use compact_str::format_compact; use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_db::files::{File, FilePath, FileRootKind}; @@ -219,27 +216,10 @@ fn resolve_module_query<'db>( return None; }; - let module = match resolved { - ResolvedName::FileModule(module) => { - tracing::trace!( - "Resolved module `{name}` to `{path}`", - path = module.file.path(db) - ); - Module::file_module( - db, - name.clone(), - module.kind, - module.search_path, - module.file, - ) - } - ResolvedName::NamespacePackage => { - tracing::trace!("Module `{name}` is a namespace package"); - Module::namespace_package(db, name.clone()) - } - }; - - Some(module) + resolved + .into_iter() + .next() + .map(|candidate| candidate.into_module(db, name.clone())) } /// Like `resolve_module_query` but for cases where it failed to resolve the module @@ -275,27 +255,10 @@ fn desperately_resolve_module<'db>( return None; }; - let module = match resolved { - ResolvedName::FileModule(module) => { - tracing::trace!( - "Resolved module `{name}` to `{path}`", - path = module.file.path(db) - ); - Module::file_module( - db, - name.clone(), - module.kind, - module.search_path, - module.file, - ) - } - ResolvedName::NamespacePackage => { - tracing::trace!("Module `{name}` is a namespace package"); - Module::namespace_package(db, name.clone()) - } - }; - - Some(module) + resolved + .into_iter() + .next() + .map(|candidate| candidate.into_module(db, name.clone())) } /// Resolves the module for the given path. @@ -1083,7 +1046,7 @@ struct ModuleNameIngredient<'db> { /// Given a module name and a list of search paths in which to lookup modules, /// attempt to resolve the module name -fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Option { +fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Option { let search_paths = search_paths(db, mode); resolve_name_impl(db, name, mode, search_paths) } @@ -1097,141 +1060,272 @@ fn desperately_resolve_name( importing_file: File, name: &ModuleName, mode: ModuleResolveMode, -) -> Option { +) -> Option { let search_paths = absolute_desperate_search_paths(db, importing_file); resolve_name_impl(db, name, mode, search_paths.iter().flatten()) } +#[derive(Debug, Clone, Copy)] +enum ResolvedModule { + NamespacePackage, + LegacyNamespacePackage(File), + RegularPackage(File), + Module(File), +} + +#[derive(Debug, Clone)] +struct ModuleResolutionCandidate { + path: ModulePath, + module: ResolvedModule, + py_typed: PyTyped, + /// Whether this candidate originated from a stub package. Stub packages + /// have priority over runtime packages regardless of search path ordering. + is_stub_package: bool, +} + +impl ModuleResolutionCandidate { + // Is this some kind of namespace package? + fn is_any_namespace_package(&self) -> bool { + match self.module { + ResolvedModule::NamespacePackage => true, + ResolvedModule::LegacyNamespacePackage(_) => true, + ResolvedModule::RegularPackage(_) => false, + ResolvedModule::Module(_) => false, + } + } + + // This is the module we were actually interested in resolving, complete the resolution + fn into_module(self, db: &'_ dyn Db, name: ModuleName) -> Module<'_> { + match self.module { + ResolvedModule::NamespacePackage => { + tracing::trace!("Resolve namespace package `{name}`"); + Module::namespace_package(db, name) + } + ResolvedModule::LegacyNamespacePackage(file) => { + // legacy namespace packages behave like regular packages + // when they're the target of the resolution + tracing::trace!( + "Resolved legacy namespace package `{name}` to `{path}`", + path = file.path(db) + ); + Module::file_module( + db, + name, + ModuleKind::Package, + self.path.into_search_path(), + file, + ) + } + ResolvedModule::RegularPackage(file) => { + tracing::trace!( + "Resolved package `{name}` to `{path}`", + path = file.path(db) + ); + Module::file_module( + db, + name, + ModuleKind::Package, + self.path.into_search_path(), + file, + ) + } + ResolvedModule::Module(file) => { + tracing::trace!("Resolved module `{name}` to `{path}`", path = file.path(db)); + Module::file_module( + db, + name, + ModuleKind::Module, + self.path.into_search_path(), + file, + ) + } + } + } + + fn missing_submodule_is_terminal(&self) -> bool { + if matches!(self.py_typed, PyTyped::Partial) { + return false; + } + + // Regular packages and modules are both terminal. A `foo.py` + // in a higher-priority search path is not shadowed by + // `foo/__init__.py` in a lower-priority one. Note that both + // shadow namespace packages. + matches!( + self.module, + ResolvedModule::RegularPackage(_) | ResolvedModule::Module(_) + ) + } + + fn to_str<'a>(&self, db: &'a dyn Db) -> Cow<'a, str> { + match self.module { + ResolvedModule::NamespacePackage => { + Cow::Owned(self.path.to_system_path().unwrap_or_default().to_string()) + } + ResolvedModule::LegacyNamespacePackage(file) => Cow::Borrowed(file.path(db).as_str()), + ResolvedModule::RegularPackage(file) => Cow::Borrowed(file.path(db).as_str()), + ResolvedModule::Module(file) => Cow::Borrowed(file.path(db).as_str()), + } + } +} + fn resolve_name_impl<'a>( db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode, search_paths: impl Iterator, -) -> Option { +) -> Option { let python_version = db.python_version(); - let resolver_state = ResolverContext::new(db, python_version, mode); + let context = ResolverContext::new(db, python_version, mode); let is_non_shadowable = mode.is_non_shadowable(python_version.minor, name.as_str()); + let mut stub_name = None; + + let mut cur_candidates = search_paths + .filter_map(|search_path| { + // When a builtin module is imported, standard module resolution is bypassed: + // the module name always resolves to the stdlib module, + // even if there's a module of the same name in the first-party root + // (which would normally result in the stdlib module being overridden). + // TODO: offer a diagnostic if there is a first-party module of the same name + if is_non_shadowable && !search_path.is_standard_library() { + return None; + } - let name = RelaxedModuleName::new(name); - let stub_name = name.to_stub_package(); - let mut is_namespace_package = false; - - for search_path in search_paths { - // When a builtin module is imported, standard module resolution is bypassed: - // the module name always resolves to the stdlib module, - // even if there's a module of the same name in the first-party root - // (which would normally result in the stdlib module being overridden). - // TODO: offer a diagnostic if there is a first-party module of the same name - if is_non_shadowable && !search_path.is_standard_library() { - continue; - } - - if !search_path.is_standard_library() && resolver_state.mode.stubs_allowed() { - match resolve_name_in_search_path(&resolver_state, &stub_name, search_path) { - Ok((package_kind, _, ResolvedName::FileModule(module))) => { - if package_kind.is_root() && module.kind.is_module() { + Some(ModuleResolutionCandidate { + path: search_path.to_module_path(), + module: ResolvedModule::NamespacePackage, + py_typed: PyTyped::Untyped, + is_stub_package: false, + }) + }) + .collect::>(); + let mut next_candidates = vec![]; + + // FIXME?: because we have to search every candidate on each step of this loop, + // in theory we can search them all in parallel. However we need to join the parallelism + // at the end of each iteration, and after the first iteration in 99% of cases we will have + // reduced down to a single candidate, so maybe meh? + let mut is_root = true; + for component in name.components() { + // Search for the next component in every search-path + for mut candidate in cur_candidates.drain(..) { + // On the first iteration, look for `mypackage-stubs` as well + // Optimization: stdlib never has these `-stubs` + let stubs_allowed = is_root + && context.mode.stubs_allowed() + && !candidate.path.search_path().is_standard_library(); + if stubs_allowed { + let stub_name = stub_name.get_or_insert_with(|| format!("{component}-stubs")); + let mut stub_candidate = candidate.clone(); + if resolve_name_in_search_path(&context, &mut stub_candidate, stub_name).is_ok() { + // `mypackage-stubs.py(i)` is not a valid result + if matches!(stub_candidate.module, ResolvedModule::Module(_)) { tracing::trace!( - "Search path `{search_path}` contains a module \ - named `{stub_name}` but a standalone module isn't a valid stub." + "Search path `{}` contains a module \ + named `{stub_name}` but a standalone module isn't a valid stub.", + candidate.path.search_path() ); } else { - return Some(ResolvedName::FileModule(module)); + stub_candidate.is_stub_package = true; + next_candidates.push(stub_candidate); + // Don't break here: we always need to process the non-stub + // candidate for the same search path, because sub-packages + // within the stubs may override py_typed to partial and fall + // through to the runtime package. } } - Ok((_, _, ResolvedName::NamespacePackage)) => { - is_namespace_package = true; - } - Err((PackageKind::Root, _)) => { - tracing::trace!( - "Search path `{search_path}` contains no stub package named `{stub_name}`." - ); - } - Err((PackageKind::Regular, PyTyped::Partial)) => { - tracing::trace!( - "Stub-package in `{search_path}` doesn't contain module: \ - `{name}` but it is a partial package, keep going." - ); - // stub exists, but the module doesn't. But this is a partial package, - // fall through to looking for a non-stub package - } - Err((PackageKind::Regular, _)) => { - tracing::trace!( - "Stub-package in `{search_path}` doesn't contain module: `{name}`" - ); - // stub exists, but the module doesn't. - return None; - } - Err((PackageKind::Namespace, _)) => { - tracing::trace!( - "Stub-package in `{search_path}` doesn't contain module: \ - `{name}` but it is a namespace package, keep going." - ); - // stub exists, but the module doesn't. But this is a namespace package, - // fall through to looking for a non-stub package - } } - } - match resolve_name_in_search_path(&resolver_state, &name, search_path) { - Ok((_, _, ResolvedName::FileModule(module))) => { - return Some(ResolvedName::FileModule(module)); + // On the root iteration when stubs are allowed, we can't break + // early because a stub package in a later search path has + // priority over a runtime package regardless of search path + // ordering. stdlib candidates are exempt since stub packages + // don't apply to stdlib modules. Thus, the `break`s below are + // guarded by `!stubs_allowed`. + + if resolve_name_in_search_path(&context, &mut candidate, component).is_err() { + if candidate.missing_submodule_is_terminal() && !stubs_allowed { + // Everything after this package should be shadowed out by + // this failure But the previous results are still in play + // because they would have shadowed this one out anyway. + break; + } + continue; } - Ok((_, _, ResolvedName::NamespacePackage)) => { - is_namespace_package = true; + let shadows_all = candidate.missing_submodule_is_terminal(); + next_candidates.push(candidate); + if shadows_all && !stubs_allowed { + break; } - Err(kind) => match kind { - (PackageKind::Root, _) => { - tracing::trace!( - "Search path `{search_path}` contains no package named `{name}`." - ); - } - (PackageKind::Regular, PyTyped::Partial) => { - tracing::trace!( - "Package in `{search_path}` doesn't contain module: \ - `{name}` but it is a partial package, keep going." - ); - } - (PackageKind::Regular, _) => { - // For regular packages, don't search the next search path. All files of that - // package must be in the same location - tracing::trace!("Package in `{search_path}` doesn't contain module: `{name}`"); - return None; - } - (PackageKind::Namespace, _) => { - tracing::trace!( - "Package in `{search_path}` doesn't contain module: \ - `{name}` but it is a namespace package, keep going." - ); - } - }, } - } - if is_namespace_package { - return Some(ResolvedName::NamespacePackage); - } - - None -} + // Now that we have several candidates, we need to reject candidates + // that are shadowed. There are only two valid situations where we + // could proceed into the next iteration with multiple candidates: + // + // * All candidates are namespace packages. + // * At least one candidate is a stub package. + // + // The existence of a single non-namespace package will shadow + // all namespace packages *regardless of search-path order*. + // + // This is implemented with the `retain` that follows. + // + // We can't do this "delete all namespace packages" eagerly because we want a + // `PyTyped::Partial` regular package to shadow namespace packages after it. + // (FIXME: I guess we could just set a flag not to add them...) + + // Note that we intentionally do *not* filter out non-stub + // candidates when a stub package is found. Even when a + // non-namespace, non-partial stub exists, we keep non-stub + // candidates as fallbacks because sub-packages within the + // stubs may override py.typed to partial. The stub candidate + // is ordered first so it takes priority. The non-stub will + // only be used when the stub fails to find a submodule in a + // partial sub-package. + + let found_non_namespace = next_candidates + .iter() + .any(|candidate| !candidate.is_any_namespace_package()); + next_candidates.retain(|candidate| { + // TODO: it might be nice to emit a warning in the case that + // we found a legacy namespace package and this candidate is + // anything *else*. When that "else" is a regular package or + // module, then the logic below will drop the legacy namespace + // package under the presumption that regular modules always shadow + // _all_ namespace packages, regardless of search path order. But + // I suppose there could be a case where we found both a legacy + // namespace package and a non-legacy namespace package (and no + // regular packages/modules). In that case, this logic currently + // retains both candidates. + + // Regular packages and modules both shadow namespace packages + // independent of search path order. + if found_non_namespace && candidate.is_any_namespace_package() { + tracing::trace!( + "Discarding namespace package `{}` \ + because a non-namespace entry of the same name was found", + candidate.to_str(db), + ); + return false; + } + true + }); -#[derive(Debug)] -enum ResolvedName { - /// A module that resolves to a file. - FileModule(ResolvedFileModule), + // Stub packages have priority over runtime packages regardless of + // search path ordering. + next_candidates.sort_by_key(|c| !c.is_stub_package); + if next_candidates.is_empty() { + return None; + } - /// The module name resolved to a namespace package. - /// - /// For example, `from opentelemetry import trace, metrics` where - /// `opentelemetry` is a namespace package (and `trace` and `metrics` are - /// sub packages). - NamespacePackage, -} + // Advance to the next level of candidates while reusing allocations + // (we used `drain` so cur_candidates is empty) + std::mem::swap(&mut cur_candidates, &mut next_candidates); + is_root = false; + } -#[derive(Debug)] -struct ResolvedFileModule { - kind: ModuleKind, - search_path: SearchPath, - file: File, + Some(cur_candidates) } /// Attempts to resolve a module name in a particular search path. @@ -1250,45 +1344,41 @@ struct ResolvedFileModule { /// Upon error, the kind of the parent package is returned. fn resolve_name_in_search_path( context: &ResolverContext, - name: &RelaxedModuleName, - search_path: &SearchPath, -) -> Result<(PackageKind, PyTyped, ResolvedName), (PackageKind, PyTyped)> { - let mut components = name.components(); - let module_name = components.next_back().unwrap(); - - let resolved_package = resolve_package(search_path, components, context)?; - - let mut package_path = resolved_package.path; - + candidate: &mut ModuleResolutionCandidate, + module_name: &str, +) -> Result<(), ()> { + if matches!(candidate.module, ResolvedModule::Module(_)) { + tracing::trace!( + "Non-package module {} cannot have a child", + candidate.to_str(context.db) + ); + return Err(()); + } + let package_path = &mut candidate.path; package_path.push(module_name); // Check for a regular package first (highest priority) package_path.push("__init__"); - if let Some(regular_package) = resolve_file_module(&package_path, context) { - return Ok(( - resolved_package.kind, - resolved_package.typed, - ResolvedName::FileModule(ResolvedFileModule { - search_path: search_path.clone(), - kind: ModuleKind::Package, - file: regular_package, - }), - )); + if let Some(init) = resolve_file_module(package_path, context) { + // Remove the `__init__` component for any potential next step + package_path.pop(); + candidate.py_typed = package_path + .py_typed(context) + .inherit_parent(candidate.py_typed); + if is_legacy_namespace_package(package_path, context, init) { + candidate.module = ResolvedModule::LegacyNamespacePackage(init); + } else { + candidate.module = ResolvedModule::RegularPackage(init); + } + return Ok(()); } // Check for a file module next package_path.pop(); - if let Some(file_module) = resolve_file_module(&package_path, context) { - return Ok(( - resolved_package.kind, - resolved_package.typed, - ResolvedName::FileModule(ResolvedFileModule { - file: file_module, - kind: ModuleKind::Module, - search_path: search_path.clone(), - }), - )); + if let Some(file_module) = resolve_file_module(package_path, context) { + candidate.module = ResolvedModule::Module(file_module); + return Ok(()); } // Last resort, check if a folder with the given name exists. If so, @@ -1308,7 +1398,7 @@ fn resolve_name_in_search_path( // simply skip this check which also helps performance. If typeshed // ever uses namespace packages, ensure that this check also takes the // `VERSIONS` file into consideration. - if !search_path.is_standard_library() && package_path.is_directory(context) { + if !package_path.search_path().is_standard_library() && package_path.is_directory(context) { if let Some(path) = package_path.to_system_path() { let system = context.db.system(); if system.case_sensitivity().is_case_sensitive() @@ -1317,18 +1407,20 @@ fn resolve_name_in_search_path( package_path.search_path().as_system_path().unwrap(), ) { - return Ok(( - resolved_package.kind, - resolved_package.typed, - ResolvedName::NamespacePackage, - )); + candidate.py_typed = package_path + .py_typed(context) + .inherit_parent(candidate.py_typed); + candidate.module = ResolvedModule::NamespacePackage; + return Ok(()); } } } - Err((resolved_package.kind, resolved_package.typed)) + Err(()) } +type ResolvedNames = Vec; + /// If `module` exists on disk with either a `.pyi` or `.py` extension, /// return the [`File`] corresponding to that path. /// @@ -1366,90 +1458,6 @@ pub(super) fn resolve_file_module( Some(file) } -/// Attempt to resolve the parent package of a module. -/// -/// `module_search_path` should be the directory to start looking for the -/// parent package. -/// -/// `components` should be the full module name of the parent package. This -/// specifically should not include the basename of the module. So e.g., -/// for `foo.bar.baz`, `components` should be `[foo, bar]`. It follows that -/// `components` may be empty (in which case, the parent package is the root). -/// -/// Upon success, the path to the package and its "kind" (root, regular or -/// namespace) is returned. Upon error, the kind of the package is still -/// returned based on how many components were found and whether `__init__.py` -/// is present. -fn resolve_package<'a, 'db, I>( - module_search_path: &SearchPath, - components: I, - resolver_state: &ResolverContext<'db>, -) -> Result -where - I: Iterator, -{ - let mut package_path = module_search_path.to_module_path(); - - // `true` if inside a folder that is a namespace package (has no `__init__.py`). - // Namespace packages are special because they can be spread across multiple search paths. - // https://peps.python.org/pep-0420/ - let mut in_namespace_package = false; - - // `true` if resolving a sub-package. For example, `true` when resolving `bar` of `foo.bar`. - let mut in_sub_package = false; - - let mut typed = package_path.py_typed(resolver_state); - - // For `foo.bar.baz`, test that `foo` and `bar` both contain a `__init__.py`. - for folder in components { - package_path.push(folder); - typed = package_path.py_typed(resolver_state).inherit_parent(typed); - - let is_regular_package = package_path.is_regular_package(resolver_state); - - if is_regular_package { - // This is the only place where we need to consider the existence of legacy namespace - // packages, as we are explicitly searching for the *parent* package of the module - // we actually want. Here, such a package should be treated as a PEP-420 ("modern") - // namespace package. In all other contexts it acts like a normal package and needs - // no special handling. - in_namespace_package = is_legacy_namespace_package(&package_path, resolver_state); - } else if package_path.is_directory(resolver_state) - // Pure modules hide namespace packages with the same name - && resolve_file_module(&package_path, resolver_state).is_none() - { - // A directory without an `__init__.py(i)` is a namespace package, - // continue with the next folder. - in_namespace_package = true; - } else if in_namespace_package { - // Package not found but it is part of a namespace package. - return Err((PackageKind::Namespace, typed)); - } else if in_sub_package { - // A regular sub package wasn't found. - return Err((PackageKind::Regular, typed)); - } else { - // We couldn't find `foo` for `foo.bar.baz`, search the next search path. - return Err((PackageKind::Root, typed)); - } - - in_sub_package = true; - } - - let kind = if in_namespace_package { - PackageKind::Namespace - } else if in_sub_package { - PackageKind::Regular - } else { - PackageKind::Root - }; - - Ok(ResolvedPackage { - kind, - path: package_path, - typed, - }) -} - /// Determines whether a package is a legacy namespace package. /// /// Before PEP 420 introduced implicit namespace packages, the ecosystem developed @@ -1479,19 +1487,14 @@ where /// we will just get confused if you mess it up). fn is_legacy_namespace_package( package_path: &ModulePath, - resolver_state: &ResolverContext, + context: &ResolverContext, + init: File, ) -> bool { // Just an optimization, the stdlib and typeshed are never legacy namespace packages if package_path.search_path().is_standard_library() { return false; } - let mut package_path = package_path.clone(); - package_path.push("__init__"); - let Some(init) = resolve_file_module(&package_path, resolver_state) else { - return false; - }; - // This is all syntax-only analysis so it *could* be fooled but it's really unlikely. // // The benefit of being syntax-only is speed and avoiding circular dependencies @@ -1499,44 +1502,13 @@ fn is_legacy_namespace_package( // // The downside is if you write slightly different syntax we will fail to detect the idiom, // but hey, this is better than nothing! - let parsed = ruff_db::parsed::parsed_module(resolver_state.db, init); + let parsed = ruff_db::parsed::parsed_module(context.db, init); let mut visitor = LegacyNamespacePackageVisitor::default(); - visitor.visit_body(parsed.load(resolver_state.db).suite()); + visitor.visit_body(parsed.load(context.db).suite()); visitor.is_legacy_namespace_package } -#[derive(Debug)] -struct ResolvedPackage { - path: ModulePath, - kind: PackageKind, - typed: PyTyped, -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -enum PackageKind { - /// A root package or module. E.g. `foo` in `foo.bar.baz` or just `foo`. - Root, - - /// A regular sub-package where the parent contains an `__init__.py`. - /// - /// For example, `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`. - Regular, - - /// A sub-package in a namespace package. A namespace package is a package - /// without an `__init__.py`. - /// - /// For example, `bar` in `foo.bar` if the `foo` directory contains no - /// `__init__.py`. - Namespace, -} - -impl PackageKind { - pub(crate) const fn is_root(self) -> bool { - matches!(self, PackageKind::Root) - } -} - /// Info about the `py.typed` file for this package #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub(crate) enum PyTyped { @@ -1587,34 +1559,6 @@ impl<'db> ResolverContext<'db> { } } -/// A [`ModuleName`] but with relaxed semantics to allow `-stubs.path` -#[derive(Debug)] -struct RelaxedModuleName(compact_str::CompactString); - -impl RelaxedModuleName { - fn new(name: &ModuleName) -> Self { - Self(name.as_str().into()) - } - - fn components(&self) -> Split<'_, char> { - self.0.split('.') - } - - fn to_stub_package(&self) -> Self { - if let Some((package, rest)) = self.0.split_once('.') { - Self(format_compact!("{package}-stubs.{rest}")) - } else { - Self(format_compact!("{package}-stubs", package = self.0)) - } - } -} - -impl fmt::Display for RelaxedModuleName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - /// Detects if a module contains a statement of the form: /// ```python /// __path__ = pkgutil.extend_path(__path__, __name__) @@ -2005,14 +1949,12 @@ mod tests { asyncio: 3.8- # 'Regular' package on py38+ asyncio.tasks: 3.9-3.11 # Submodule on py39+ only functools: 3.8- # Top-level single-file module - xml: 3.8-3.8 # Namespace package on py38 only "; const STDLIB: &[FileSpec] = &[ ("asyncio/__init__.pyi", ""), ("asyncio/tasks.pyi", ""), ("functools.pyi", ""), - ("xml/etree.pyi", ""), ]; const TYPESHED: MockedTypeshed = MockedTypeshed { @@ -2025,7 +1967,7 @@ mod tests { .with_python_version(PythonVersion::PY38) .build(); - let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]); + let existing_modules = create_module_names(&["asyncio", "functools"]); for module_name in existing_modules { let resolved_module = resolve_module_confident(&db, &module_name).unwrap_or_else(|| { @@ -2049,16 +1991,12 @@ mod tests { asyncio: 3.8- # 'Regular' package on py38+ asyncio.tasks: 3.9-3.11 # Submodule on py39+ only collections: 3.9- # 'Regular' package on py39+ - importlib: 3.9- # Namespace package on py39+ - xml: 3.8-3.8 # Namespace package on 3.8 only "; const STDLIB: &[FileSpec] = &[ ("collections/__init__.pyi", ""), ("asyncio/__init__.pyi", ""), ("asyncio/tasks.pyi", ""), - ("importlib/abc.pyi", ""), - ("xml/etree.pyi", ""), ]; const TYPESHED: MockedTypeshed = MockedTypeshed { @@ -2071,13 +2009,7 @@ mod tests { .with_python_version(PythonVersion::PY38) .build(); - let nonexisting_modules = create_module_names(&[ - "collections", - "importlib", - "importlib.abc", - "xml", - "asyncio.tasks", - ]); + let nonexisting_modules = create_module_names(&["collections", "asyncio.tasks"]); for module_name in nonexisting_modules { assert!( @@ -2094,7 +2026,6 @@ mod tests { asyncio.tasks: 3.9-3.11 # Submodule on py39+ only collections: 3.9- # 'Regular' package on py39+ functools: 3.8- # Top-level single-file module - importlib: 3.9- # Namespace package on py39+ "; const STDLIB: &[FileSpec] = &[ @@ -2102,7 +2033,6 @@ mod tests { ("asyncio/tasks.pyi", ""), ("collections/__init__.pyi", ""), ("functools.pyi", ""), - ("importlib/abc.pyi", ""), ]; const TYPESHED: MockedTypeshed = MockedTypeshed { @@ -2115,13 +2045,8 @@ mod tests { .with_python_version(PythonVersion::PY39) .build(); - let existing_modules = create_module_names(&[ - "asyncio", - "functools", - "importlib.abc", - "collections", - "asyncio.tasks", - ]); + let existing_modules = + create_module_names(&["asyncio", "functools", "collections", "asyncio.tasks"]); for module_name in existing_modules { let resolved_module = diff --git a/crates/ty_python_semantic/resources/mdtest/import/namespace.md b/crates/ty_python_semantic/resources/mdtest/import/namespace.md index 8949d26512831..7a4bea91f0e8a 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/namespace.md +++ b/crates/ty_python_semantic/resources/mdtest/import/namespace.md @@ -146,3 +146,70 @@ from opentelemetry import trace, metrics reveal_type(trace) # revealed: reveal_type(metrics) # revealed: ``` + +## Priority across search paths + +According PEP 420, namespace packages always have lower precedence than normal packages/modules, +regardless of search path ordering. + +These are regression tests for + +### Namespace package comes first on the search path + +```toml +[environment] +extra-paths = ["/path-one", "/path-two"] +``` + +`/path-one/mod/sub1.py`: + +```py +``` + +`/path-two/mod/__init__.py`: + +```py +``` + +`/path-two/mod/sub2.py`: + +```py +``` + +`main.py`: + +```py +import mod +import mod.sub1 # error: [unresolved-import] +import mod.sub2 +``` + +### Namespace package comes last on the search path + +```toml +[environment] +extra-paths = ["/path-two", "/path-one"] +``` + +`/path-one/mod/sub1.py`: + +```py +``` + +`/path-two/mod/__init__.py`: + +```py +``` + +`/path-two/mod/sub2.py`: + +```py +``` + +`main.py`: + +```py +import mod +import mod.sub1 # error: [unresolved-import] +import mod.sub2 +``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md index 3f99e5d92b54a..ee253c350a5c5 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md +++ b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md @@ -191,8 +191,7 @@ reveal_type(Hexagon().area) # revealed: Unknown The runtime package is a regular package but the stubs are namespace packages. Pyright skips the stub package if the "regular" package isn't a namespace package. I'm not aware that the behavior -here is specified, and using the stubs without probing the runtime package first requires slightly -fewer lookups. +here is specified, but we currently agree with pyright here. ```toml [environment] @@ -202,17 +201,13 @@ extra-paths = ["/packages"] `/packages/shapes-stubs/polygons/pentagon.pyi`: ```pyi -class Pentagon: - sides: int - area: float +class Pentagon: ... ``` `/packages/shapes-stubs/polygons/hexagon.pyi`: ```pyi -class Hexagon: - sides: int - area: float +class Hexagon: ... ``` `/packages/shapes/__init__.py`: @@ -228,13 +223,17 @@ class Hexagon: `/packages/shapes/polygons/pentagon.py`: ```py -class Pentagon: ... +class Pentagon: + sides: int + area: float ``` `/packages/shapes/polygons/hexagon.py`: ```py -class Hexagon: ... +class Hexagon: + sides: int + area: float ``` `main.py`: @@ -313,3 +312,107 @@ import yaml reveal_type(yaml.YamlLoader) # revealed: ``` + +## Priority across search paths + +Arguably [import resolution ordering], while vague, implies that a `foo-stubs` stub package should +have priority over a `foo` package regardless of search path ordering. + +Regression test for + +### Stub package comes first on the search path + +```toml +[environment] +extra-paths = ["/path-one", "/path-two"] +``` + +`/path-one/shapes-stubs/__init__.pyi`: + +```pyi +class Pentagon: + sides: int +``` + +`/path-two/shapes/__init__.py`: + +```py +class Pentagon: ... +``` + +`main.py`: + +```py +from shapes import Pentagon + +reveal_type(Pentagon().sides) # revealed: int +``` + +### Stub package comes last on the search path + +```toml +[environment] +extra-paths = ["/path-two", "/path-one"] +``` + +`/path-one/shapes-stubs/__init__.pyi`: + +```pyi +class Pentagon: + sides: int +``` + +`/path-two/shapes/__init__.py`: + +```py +class Pentagon: ... +``` + +`main.py`: + +```py +from shapes import Pentagon + +reveal_type(Pentagon().sides) # revealed: int +``` + +### Partial stub packages + +Because `shapes/bar.pyi` is a stub file, it must take priority over `shapes/foo.py` in the first +search path even though `shapes/bar.pyi` appears in the second search path. But because +`shapes/bar.pyi` is a `partial = true` namespace package, when we fail to find the `foo` submodule +in `/path-two/shapes`, we must fallback to `shapes/foo.py` when resolving the module. + +This test exists at the intersection of namespace packages and partial stub packages. + +```toml +[environment] +extra-paths = ["/path-one", "/path-two"] +``` + +`/path-one/shapes/foo.py`: + +```py +X = 42 +``` + +`/path-two/shapes/bar.pyi`: + +```pyi +``` + +`/path-two/shapes/py.typed`: + +```text +partial = true +``` + +`main.py`: + +```py +from shapes.foo import X + +reveal_type(X) # revealed: Literal[42] +``` + +[import resolution ordering]: https://typing.python.org/en/latest/spec/distributing.html#import-resolution-ordering From 34cee06dfa6c558c4ab1460200033ea44b368ae4 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 4 Mar 2026 19:10:45 +0000 Subject: [PATCH 199/261] [ty] Split up `types/class.rs` (#23714) --- crates/ty_python_semantic/src/types.rs | 2 +- crates/ty_python_semantic/src/types/class.rs | 6375 +---------------- .../src/types/class/dynamic_literal.rs | 509 ++ .../src/types/class/known.rs | 1912 +++++ .../src/types/class/named_tuple.rs | 582 ++ .../src/types/class/static_literal.rs | 3129 ++++++++ 6 files changed, 6304 insertions(+), 6205 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/class/dynamic_literal.rs create mode 100644 crates/ty_python_semantic/src/types/class/known.rs create mode 100644 crates/ty_python_semantic/src/types/class/named_tuple.rs create mode 100644 crates/ty_python_semantic/src/types/class/static_literal.rs diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6aa64498291ee..02005655fdba7 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -65,7 +65,7 @@ use crate::types::generics::{ pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; use crate::types::known_instance::{InternedConstraintSet, InternedType, UnionTypeInstance}; pub use crate::types::method::{BoundMethodType, KnownBoundMethodType, WrapperDescriptorKind}; -use crate::types::mro::{Mro, MroIterator, StaticMroError}; +use crate::types::mro::{MroIterator, StaticMroError}; pub(crate) use crate::types::narrow::{ NarrowingConstraint, PossiblyNarrowedPlaces, PossiblyNarrowedPlacesBuilder, infer_narrowing_constraint, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 49f0e80484bfd..627c81826cb3a 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1,184 +1,58 @@ -use std::borrow::Cow; -use std::cell::RefCell; use std::fmt::Write; -use std::sync::{LazyLock, Mutex}; +pub(crate) use self::dynamic_literal::{ + DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, +}; +pub use self::known::KnownClass; +use self::named_tuple::synthesize_namedtuple_class_member; +pub(super) use self::named_tuple::{ + DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, NamedTupleField, NamedTupleSpec, +}; +pub(crate) use self::static_literal::StaticClassLiteral; use super::{ - BoundTypeVarInstance, MemberLookupPolicy, Mro, MroIterator, SpecialFormType, StaticMroError, - SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, - function::FunctionType, + BoundTypeVarInstance, MemberLookupPolicy, MroIterator, SpecialFormType, SubclassOfType, Type, + TypeQualifiers, class_base::ClassBase, function::FunctionType, }; use super::{TypeVarVariance, display}; use crate::place::{DefinedPlace, TypeOrigin}; -use crate::semantic_index::definition::{Definition, DefinitionState}; -use crate::semantic_index::scope::{NodeWithScopeKind, Scope}; -use crate::semantic_index::symbol::Symbol; -use crate::semantic_index::{ - DeclarationWithConstraint, SemanticIndex, attribute_declarations, attribute_scopes, -}; -use crate::types::bound_super::BoundSuperError; +use crate::semantic_index::definition::Definition; use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; -use crate::types::context::InferContext; -use crate::types::diagnostic::{INVALID_DATACLASS_OVERRIDE, SUPER_CALL_IN_NAMED_TUPLE_METHOD}; -use crate::types::enums::{ - enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value, -}; -use crate::types::function::{ - AbstractMethodKind, DataclassTransformerFlags, DataclassTransformerParams, KnownFunction, - is_implicit_classmethod, is_implicit_staticmethod, -}; +use crate::types::function::{AbstractMethodKind, DataclassTransformerParams}; use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, walk_specialization, }; -use crate::types::infer::{infer_expression_type, infer_unpack_types, nearest_enclosing_class}; use crate::types::known_instance::DeprecatedInstance; -use crate::types::member::{Member, class_member}; -use crate::types::mro::DynamicMroError; +use crate::types::member::Member; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; -use crate::types::tuple::{Tuple, TupleSpec, TupleType}; -use crate::types::typed_dict::{TypedDictParams, typed_dict_params_from_class_def}; -use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; +use crate::types::tuple::TupleSpec; use crate::types::{ - ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType, CallableTypes, - DATACLASS_FLAGS, DataclassFlags, DataclassParams, FindLegacyTypeVarsVisitor, - IntersectionBuilder, KnownInstanceType, MaterializationKind, PropertyInstanceType, TypeContext, - TypeMapping, UnionBuilder, VarianceInferable, binding_type, declaration_type, - determine_upper_bound, + ApplyTypeMappingVisitor, CallableType, CallableTypes, DataclassParams, + FindLegacyTypeVarsVisitor, IntersectionBuilder, TypeContext, TypeMapping, UnionBuilder, + VarianceInferable, }; use crate::{ - Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, + Db, FxIndexMap, FxOrderSet, place::{ Definedness, LookupError, LookupResult, Place, PlaceAndQualifiers, Widening, - known_module_symbol, place_from_bindings, place_from_declarations, - }, - semantic_index::{ - attribute_assignments, - definition::{DefinitionKind, TargetKind}, - place_table, - scope::ScopeId, - semantic_index, use_def_map, - }, - types::{ - CallArguments, CallError, CallErrorKind, MetaclassCandidate, MetaclassTransformInfo, - TypeDefinition, UnionType, definition_expression_type, + place_from_bindings, place_from_declarations, }, + semantic_index::{place_table, use_def_map}, + types::{MetaclassCandidate, TypeDefinition, UnionType}, }; -use indexmap::IndexSet; -use itertools::{Either, Itertools as _}; use ruff_db::diagnostic::Span; use ruff_db::files::File; -use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, NodeIndex, PythonVersion}; -use ruff_text_size::{Ranged, TextRange}; -use rustc_hash::FxHashSet; -use ty_module_resolver::{KnownModule, file_to_module}; - -fn implicit_attribute_initial<'db>( - _db: &'db dyn Db, - id: salsa::Id, - _class_body_scope: ScopeId<'db>, - _name: String, - _target_method_decorator: MethodDecorator, -) -> Member<'db> { - Member { - inner: Place::bound(Type::divergent(id)).into(), - } -} - -#[allow(clippy::too_many_arguments)] -fn implicit_attribute_cycle_recover<'db>( - db: &'db dyn Db, - cycle: &salsa::Cycle, - previous_member: &Member<'db>, - member: Member<'db>, - _class_body_scope: ScopeId<'db>, - _name: String, - _target_method_decorator: MethodDecorator, -) -> Member<'db> { - let inner = member - .inner - .cycle_normalized(db, previous_member.inner, cycle); - Member { inner } -} - -fn static_class_try_mro_cycle_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - self_: StaticClassLiteral<'db>, - specialization: Option>, -) -> Result, StaticMroError<'db>> { - Err(StaticMroError::cycle( - db, - self_.apply_optional_specialization(db, specialization), - )) -} +use ruff_python_ast::{self as ast}; +use ruff_text_size::TextRange; -#[allow(clippy::unnecessary_wraps)] -fn try_metaclass_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self_: StaticClassLiteral<'db>, -) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { - Err(MetaclassError { - kind: MetaclassErrorKind::Cycle, - }) -} - -fn explicit_bases_cycle_initial<'db>( - db: &'db dyn Db, - id: salsa::Id, - literal: StaticClassLiteral<'db>, -) -> Box<[Type<'db>]> { - let module = parsed_module(db, literal.file(db)).load(db); - let class_stmt = literal.node(db, &module); - // Try to produce a list of `Divergent` types of the right length. However, if one or more of - // the bases is a starred expression, we don't know how many entries that will eventually - // expand to. - vec![Type::divergent(id); class_stmt.bases().len()].into_boxed_slice() -} - -fn explicit_bases_cycle_fn<'db>( - db: &'db dyn Db, - cycle: &salsa::Cycle, - previous: &[Type<'db>], - current: Box<[Type<'db>]>, - _literal: StaticClassLiteral<'db>, -) -> Box<[Type<'db>]> { - if previous.len() == current.len() { - // As long as the length of bases hasn't changed, use the same "monotonic widening" - // strategy that we use with most types, to avoid oscillations. - current - .iter() - .zip(previous.iter()) - .map(|(curr, prev)| curr.cycle_normalized(db, *prev, cycle)) - .collect() - } else { - // The length of bases has changed, presumably because we expanded a starred expression. We - // don't do "monotonic widening" here, because we don't want to make assumptions about - // which previous entries correspond to which current ones. An oscillation here would be - // unfortunate, but maybe only pathological programs can trigger such a thing. - current - } -} - -#[expect(clippy::unnecessary_wraps)] -fn dynamic_class_try_mro_cycle_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - self_: DynamicClassLiteral<'db>, -) -> Result, DynamicMroError<'db>> { - // When there's a cycle, return a minimal MRO with just the class itself and object. - // This breaks the cycle and allows type checking to continue. - Ok(Mro::from([ - ClassBase::Class(ClassType::NonGeneric(self_.into())), - ClassBase::object(db), - ])) -} +mod dynamic_literal; +mod known; +mod named_tuple; +mod static_literal; /// A category of classes with code generation capabilities (with synthesized methods). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] @@ -2147,4178 +2021,174 @@ impl<'db> Field<'db> { } } -/// Representation of a class definition statement in the AST: either a non-generic class, or a -/// generic class that has not been specialized. -/// -/// This does not in itself represent a type, but can be transformed into a [`ClassType`] that -/// does. (For generic classes, this requires specializing its generic context.) -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct StaticClassLiteral<'db> { - /// Name of the class at definition - #[returns(ref)] - pub(crate) name: Name, - - pub(crate) body_scope: ScopeId<'db>, - - pub(crate) known: Option, - - /// If this class is deprecated, this holds the deprecation message. - pub(crate) deprecated: Option>, - - pub(crate) type_check_only: bool, - - pub(crate) dataclass_params: Option>, - pub(crate) dataclass_transformer_params: Option>, - - /// Whether this class is decorated with `@functools.total_ordering` - pub(crate) total_ordering: bool, +impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { + fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { + match self { + Self::Static(class) => class.variance_of(db, typevar), + Self::Dynamic(_) | Self::DynamicNamedTuple(_) => TypeVarVariance::Bivariant, + } + } } -// The Salsa heap is tracked separately. -impl get_size2::GetSize for StaticClassLiteral<'_> {} - -fn generic_context_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self: StaticClassLiteral<'db>, -) -> Option> { - None +/// Performs member lookups over an MRO (Method Resolution Order). +/// +/// This struct encapsulates the shared logic for looking up class and instance +/// members by iterating through an MRO. Both `StaticClassLiteral` and `DynamicClassLiteral` +/// use this to avoid duplicating the MRO traversal logic. +pub(super) struct MroLookup<'db, I> { + db: &'db dyn Db, + mro_iter: I, } -#[salsa::tracked] -impl<'db> StaticClassLiteral<'db> { - /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { - self.known(db) == Some(known_class) - } - - pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { - self.is_known(db, KnownClass::Tuple) +impl<'db, I: Iterator>> MroLookup<'db, I> { + /// Create a new MRO lookup from a database and an MRO iterator. + pub(super) fn new(db: &'db dyn Db, mro_iter: I) -> Self { + Self { db, mro_iter } } - /// Returns `true` if this class inherits from a functional namedtuple - /// (`DynamicNamedTupleLiteral`) that has unknown fields. + /// Look up a class member by iterating through the MRO. /// - /// When the base namedtuple's fields were determined dynamically (e.g., from a variable), - /// we can't synthesize precise method signatures and should fall back to `NamedTupleFallback`. - pub(crate) fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { - self.explicit_bases(db).iter().any(|base| match base { - Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) => { - !namedtuple.has_known_fields(db) - } - _ => false, - }) - } - - /// Returns `true` if this class is a dataclass-like class. + /// Parameters: + /// - `name`: The member name to look up + /// - `policy`: Controls which classes in the MRO to skip + /// - `inherited_generic_context`: Generic context for `own_class_member` calls + /// - `is_self_object`: Whether the class itself is `object` (affects policy filtering) /// - /// This covers `@dataclass`-decorated classes, as well as classes created via - /// `dataclass_transform` (function-based, metaclass-based, and base-class-based). - pub(crate) fn is_dataclass_like(self, db: &'db dyn Db) -> bool { - matches!( - CodeGeneratorKind::from_class(db, ClassLiteral::Static(self), None), - Some(CodeGeneratorKind::DataclassLike(_)) - ) - } - - /// Returns a new [`StaticClassLiteral`] with the given dataclass params, preserving all other fields. - pub(crate) fn with_dataclass_params( + /// Returns `ClassMemberResult::TypedDict` if a `TypedDict` base is encountered, + /// allowing the caller to handle this case specially. + /// + /// If we encounter a dynamic type in the MRO, we save it and after traversal: + /// 1. Use it as the type if no other classes define the attribute, or + /// 2. Intersect it with the type from non-dynamic MRO members. + pub(super) fn class_member( self, - db: &'db dyn Db, - dataclass_params: Option>, - ) -> Self { - Self::new( - db, - self.name(db).clone(), - self.body_scope(db), - self.known(db), - self.deprecated(db), - self.type_check_only(db), - dataclass_params, - self.dataclass_transformer_params(db), - self.total_ordering(db), - ) - } - - /// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`, - /// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if - /// synthesis is valid. - #[salsa::tracked] - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { - let body_scope = self.body_scope(db); - ["__lt__", "__le__", "__gt__", "__ge__"] - .iter() - .any(|method| !class_member(db, body_scope, method).is_undefined()) - } + name: &str, + policy: MemberLookupPolicy, + inherited_generic_context: Option>, + is_self_object: bool, + ) -> ClassMemberResult<'db> { + let db = self.db; + let mut dynamic_type: Option> = None; + let mut lookup_result: LookupResult<'db> = + Err(LookupError::Undefined(TypeQualifiers::empty())); - /// Returns `true` if any class in this class's MRO (excluding `object`) defines an ordering - /// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation. - pub(super) fn has_ordering_method_in_mro( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> bool { - self.total_ordering_root_method(db, specialization) - .is_some() - } + for superclass in self.mro_iter { + match superclass { + ClassBase::Generic | ClassBase::Protocol => { + // Skip over these very special class bases that aren't really classes. + } + ClassBase::Dynamic(_) => { + // Note: calling `Type::from(superclass).member()` would be incorrect here. + // What we'd really want is a `Type::Any.own_class_member()` method, + // but adding such a method wouldn't make much sense -- it would always return `Any`! + dynamic_type.get_or_insert(Type::from(superclass)); + } + ClassBase::Class(class) => { + let known = class.known(db); - /// Returns the type of the ordering method used by `@total_ordering`, if any. - /// - /// Following `functools.total_ordering` precedence, we prefer `__lt__` > `__le__` > `__gt__` > - /// `__ge__`, regardless of whether the method is defined locally or inherited. - /// - /// Note: We use direct scope lookups here to avoid infinite recursion - /// through `own_class_member` -> `own_synthesized_member`. - pub(super) fn total_ordering_root_method( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> Option> { - const ORDERING_METHODS: [&str; 4] = ["__lt__", "__le__", "__gt__", "__ge__"]; + // Only exclude `object` members if this is not an `object` class itself + if known == Some(KnownClass::Object) + && policy.mro_no_object_fallback() + && !is_self_object + { + continue; + } - for name in ORDERING_METHODS { - for base in self.iter_mro(db, specialization) { - let Some(base_class) = base.into_class() else { - continue; - }; - match base_class.class_literal(db) { - ClassLiteral::Static(base_literal) => { - if base_literal.is_known(db, KnownClass::Object) { - continue; - } - let member = class_member(db, base_literal.body_scope(db), name); - if let Some(ty) = member.ignore_possibly_undefined() { - let base_specialization = base_class - .static_class_literal(db) - .and_then(|(_, spec)| spec); - return Some(ty.apply_optional_specialization(db, base_specialization)); - } + if known == Some(KnownClass::Type) && policy.meta_class_no_type_fallback() { + continue; } - ClassLiteral::Dynamic(dynamic) => { - // Dynamic classes (created with `type()`) can also define ordering methods - // in their namespace dict. - let member = dynamic.own_class_member(db, name); - if let Some(ty) = member.ignore_possibly_undefined() { - return Some(ty); - } + + if matches!(known, Some(KnownClass::Int | KnownClass::Str)) + && policy.mro_no_int_or_str_fallback() + { + continue; } - // Dynamic namedtuples don't define their own ordering methods. - ClassLiteral::DynamicNamedTuple(_) => {} + + lookup_result = lookup_result.or_else(|lookup_error| { + lookup_error.or_fall_back_to( + db, + class + .own_class_member(db, inherited_generic_context, name) + .inner, + ) + }); + } + ClassBase::TypedDict => { + return ClassMemberResult::TypedDict; } } + if lookup_result.is_ok() { + break; + } } - None - } - - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code - // the knowledge that this class is not generic. - if self.is_known(db, KnownClass::VersionInfo) { - return None; - } - - // We've already verified that the class literal does not contain both a PEP-695 generic - // scope and a `typing.Generic` base class. - // - // Note that if a class has an explicit legacy generic context (by inheriting from - // `typing.Generic`), and also an implicit one (by inheriting from other generic classes, - // specialized by typevars), the explicit one takes precedence. - self.pep695_generic_context(db) - .or_else(|| self.legacy_generic_context(db)) - .or_else(|| self.inherited_legacy_generic_context(db)) - } - - pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { - self.pep695_generic_context(db).is_some() - } - - #[salsa::tracked(cycle_initial=generic_context_cycle_initial, - heap_size=ruff_memory_usage::heap_size, - )] - pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option> { - let scope = self.body_scope(db); - let file = scope.file(db); - let parsed = parsed_module(db, file).load(db); - let class_def_node = scope.node(db).expect_class().node(&parsed); - class_def_node.type_params.as_ref().map(|type_params| { - let index = semantic_index(db, scope.file(db)); - let definition = index.expect_single_definition(class_def_node); - GenericContext::from_type_params(db, index, definition, type_params) - }) - } - - pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { - self.explicit_bases(db).iter().find_map(|base| match base { - Type::KnownInstance( - KnownInstanceType::SubscriptedGeneric(generic_context) - | KnownInstanceType::SubscriptedProtocol(generic_context), - ) => Some(*generic_context), - _ => None, + ClassMemberResult::Done(CompletedMemberLookup { + lookup_result, + dynamic_type, }) } - #[salsa::tracked(cycle_initial=generic_context_cycle_initial, - heap_size=ruff_memory_usage::heap_size, - )] - pub(crate) fn inherited_legacy_generic_context( - self, - db: &'db dyn Db, - ) -> Option> { - GenericContext::from_base_classes( - db, - self.definition(db), - self.explicit_bases(db) - .iter() - .copied() - .filter(|ty| matches!(ty, Type::GenericAlias(_))), - ) - } + /// Look up an instance member by iterating through the MRO. + /// + /// Unlike class member lookup, instance member lookup: + /// - Uses `own_instance_member` to check each class + /// - Builds a union of inferred types from multiple classes + /// - Stops on the first definitely-declared attribute + /// + /// Returns `InstanceMemberResult::TypedDict` if a `TypedDict` base is encountered, + /// allowing the caller to handle this case specially. + pub(super) fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { + let db = self.db; + let mut union = UnionBuilder::new(db); + let mut union_qualifiers = TypeQualifiers::empty(); + let mut is_definitely_bound = false; - /// Returns all of the typevars that are referenced in this class's base class list. - /// (This is used to ensure that classes do not reference typevars from enclosing - /// generic contexts.) - pub(crate) fn typevars_referenced_in_bases( - self, - db: &'db dyn Db, - ) -> FxIndexSet> { - #[derive(Default)] - struct CollectTypeVars<'db> { - typevars: RefCell>>, - recursion_guard: TypeCollector<'db>, - } + for superclass in self.mro_iter { + match superclass { + ClassBase::Generic | ClassBase::Protocol => { + // Skip over these very special class bases that aren't really classes. + } + ClassBase::Dynamic(_) => { + // We already return the dynamic type for class member lookup, so we can + // just return unbound here (to avoid having to build a union of the + // dynamic type with itself). + return InstanceMemberResult::Done(PlaceAndQualifiers::unbound()); + } + ClassBase::Class(class) => { + if let member @ PlaceAndQualifiers { + place: + Place::Defined(DefinedPlace { + ty, + origin, + definedness: boundness, + .. + }), + qualifiers, + } = class.own_instance_member(db, name).inner + { + if boundness == Definedness::AlwaysDefined { + if origin.is_declared() { + // We found a definitely-declared attribute. Discard possibly collected + // inferred types from subclasses and return the declared type. + return InstanceMemberResult::Done(member); + } - impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { - fn should_visit_lazy_type_attributes(&self) -> bool { - false - } + is_definitely_bound = true; + } - fn visit_bound_type_var_type( - &self, - _db: &'db dyn Db, - bound_typevar: BoundTypeVarInstance<'db>, - ) { - self.typevars.borrow_mut().insert(bound_typevar); - } + // If the attribute is not definitely declared on this class, keep looking + // higher up in the MRO, and build a union of all inferred types (and + // possibly-declared types): + union = union.add(ty); - fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { - walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); - } - } - - let visitor = CollectTypeVars::default(); - for base in self.explicit_bases(db) { - visitor.visit_type(db, *base); - } - visitor.typevars.into_inner() - } - - /// Returns the generic context that should be inherited by any constructor methods of this class. - fn inherited_generic_context(self, db: &'db dyn Db) -> Option> { - self.generic_context(db) - } - - pub(super) fn file(self, db: &dyn Db) -> File { - self.body_scope(db).file(db) - } - - /// Return the original [`ast::StmtClassDef`] node associated with this class - /// - /// ## Note - /// Only call this function from queries in the same file or your - /// query depends on the AST of another file (bad!). - fn node<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef { - let scope = self.body_scope(db); - scope.node(db).expect_class().node(module) - } - - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - let body_scope = self.body_scope(db); - let index = semantic_index(db, body_scope.file(db)); - index.expect_single_definition(body_scope.node(db).expect_class()) - } - - pub(crate) fn apply_specialization( - self, - db: &'db dyn Db, - f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, - ) -> ClassType<'db> { - match self.generic_context(db) { - None => ClassType::NonGeneric(self.into()), - Some(generic_context) => { - let specialization = f(generic_context); - - ClassType::Generic(GenericAlias::new(db, self, specialization)) - } - } - } - - pub(crate) fn apply_optional_specialization( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - specialization - .unwrap_or_else(|| generic_context.default_specialization(db, self.known(db))) - }) - } - - pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context - .default_specialization(db, self.known(db)) - .materialize_impl( - db, - MaterializationKind::Top, - &ApplyTypeMappingVisitor::default(), - ) - }) - } - - /// Returns the default specialization of this class. For non-generic classes, the class is - /// returned unchanged. For a non-specialized generic class, we return a generic alias that - /// applies the default specialization to the class's typevars. - pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.default_specialization(db, self.known(db)) - }) - } - - /// Returns the unknown specialization of this class. For non-generic classes, the class is - /// returned unchanged. For a non-specialized generic class, we return a generic alias that - /// maps each of the class's typevars to `Unknown`. - pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.unknown_specialization(db) - }) - } - - /// Returns a specialization of this class where each typevar is mapped to itself. - pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.identity_specialization(db) - }) - } - - /// Return an iterator over the inferred types of this class's *explicit* bases. - /// - /// Note that any class (except for `object`) that has no explicit - /// bases will implicitly inherit from `object` at runtime. Nonetheless, - /// this method does *not* include `object` in the bases it iterates over. - /// - /// ## Why is this a salsa query? - /// - /// This is a salsa query to short-circuit the invalidation - /// when the class's AST node changes. - /// - /// Were this not a salsa query, then the calling query - /// would depend on the class's AST and rerun for every change in that file. - #[salsa::tracked(returns(deref), cycle_initial=explicit_bases_cycle_initial, cycle_fn=explicit_bases_cycle_fn, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - tracing::trace!( - "StaticClassLiteral::explicit_bases_query: {}", - self.name(db) - ); - - let module = parsed_module(db, self.file(db)).load(db); - let class_stmt = self.node(db, &module); - - let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); - - match self.known(db) { - Some(KnownClass::VersionInfo) => { - let tuple_type = TupleType::new(db, &TupleSpec::version_info_spec(db)) - .expect("sys.version_info tuple spec should always be a valid tuple"); - - Box::new([ - definition_expression_type(db, class_definition, &class_stmt.bases()[0]), - Type::from(tuple_type.to_class_type(db)), - ]) - } - // Special-case `NotImplementedType`: typeshed says that it inherits from `Any`, - // but this causes more problems than it fixes. - Some(KnownClass::NotImplementedType) => Box::new([]), - _ => class_stmt - .bases() - .iter() - .flat_map(|base_node| { - if let ast::Expr::Starred(starred) = base_node { - let starred_ty = - definition_expression_type(db, class_definition, &starred.value); - // If the starred expression is a fixed-length tuple, unpack it. - if let Some(Tuple::Fixed(tuple)) = starred_ty - .tuple_instance_spec(db) - .map(std::borrow::Cow::into_owned) - { - return Either::Left(tuple.owned_elements().into_vec().into_iter()); - } - // Otherwise, we can't statically determine the bases. - Either::Right(std::iter::once(Type::unknown())) - } else { - Either::Right(std::iter::once(definition_expression_type( - db, - class_definition, - base_node, - ))) - } - }) - .collect(), - } - } - - /// Return `Some()` if this class is known to be a [`DisjointBase`], or `None` if it is not. - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { - if self - .known_function_decorators(db) - .contains(&KnownFunction::DisjointBase) - { - Some(DisjointBase::due_to_decorator(self)) - } else if SlotsKind::from(db, self) == SlotsKind::NotEmpty { - Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Static( - self, - ))) - } else { - None - } - } - - /// Iterate over this class's explicit bases, filtering out any bases that are not class - /// objects, and applying default specialization to any unspecialized generic class literals. - fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { - self.explicit_bases(db) - .iter() - .copied() - .filter_map(|ty| ty.to_class_type(db)) - } - - /// Determine if this class is a protocol. - /// - /// This method relies on the accuracy of the [`KnownClass::is_protocol`] method, - /// which hardcodes knowledge about certain special-cased classes. See the docs on - /// that method for why we do this rather than relying on generalised logic for all - /// classes, including the special-cased ones that are included in the [`KnownClass`] - /// enum. - pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { - self.known(db) - .map(KnownClass::is_protocol) - .unwrap_or_else(|| { - // Iterate through the last three bases of the class - // searching for `Protocol` or `Protocol[]` in the bases list. - // - // If `Protocol` is present in the bases list of a valid protocol class, it must either: - // - // - be the last base - // - OR be the last-but-one base (with the final base being `Generic[]` or `object`) - // - OR be the last-but-two base (with the penultimate base being `Generic[]` - // and the final base being `object`) - self.explicit_bases(db).iter().rev().take(3).any(|base| { - matches!( - base, - Type::SpecialForm(SpecialFormType::Protocol) - | Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)) - ) - }) - }) - } - - /// Return the types of the decorators on this class - #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] - fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - tracing::trace!("StaticClassLiteral::decorators: {}", self.name(db)); - - let module = parsed_module(db, self.file(db)).load(db); - - let class_stmt = self.node(db, &module); - if class_stmt.decorator_list.is_empty() { - return Box::new([]); - } - - let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); - - class_stmt - .decorator_list - .iter() - .map(|decorator_node| { - definition_expression_type(db, class_definition, &decorator_node.expression) - }) - .collect() - } - - pub(super) fn known_function_decorators( - self, - db: &'db dyn Db, - ) -> impl Iterator + 'db { - self.decorators(db) - .iter() - .filter_map(|deco| deco.as_function_literal()) - .filter_map(|decorator| decorator.known(db)) - } - - /// Iterate through the decorators on this class, returning the position of the first one - /// that matches the given predicate. - pub(super) fn find_decorator_position( - self, - db: &'db dyn Db, - predicate: impl Fn(Type<'db>) -> bool, - ) -> Option { - self.decorators(db) - .iter() - .position(|decorator| predicate(*decorator)) - } - - /// Iterate through the decorators on this class, returning the index of the first one - /// that is either `@dataclass` or `@dataclass(...)`. - pub(super) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { - self.find_decorator_position(db, |ty| match ty { - Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass), - Type::DataclassDecorator(_) => true, - _ => false, - }) - } - - /// Is this class final? - pub(super) fn is_final(self, db: &'db dyn Db) -> bool { - self.known_function_decorators(db) - .contains(&KnownFunction::Final) - || enum_metadata(db, ClassLiteral::Static(self)).is_some() - } - - /// Attempt to resolve the [method resolution order] ("MRO") for this class. - /// If the MRO is unresolvable, return an error indicating why the class's MRO - /// cannot be accurately determined. The error returned contains a fallback MRO - /// that will be used instead for the purposes of type inference. - /// - /// The MRO is the tuple of classes that can be retrieved as the `__mro__` - /// attribute on a class at runtime. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - #[salsa::tracked(returns(as_ref), cycle_initial=static_class_try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn try_mro( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> Result, StaticMroError<'db>> { - tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db)); - Mro::of_static_class(db, self, specialization) - } - - /// Iterate over the [method resolution order] ("MRO") of the class. - /// - /// If the MRO could not be accurately resolved, this method falls back to iterating - /// over an MRO that has the class directly inheriting from `Unknown`. Use - /// [`StaticClassLiteral::try_mro`] if you need to distinguish between the success and failure - /// cases rather than simply iterating over the inferred resolution order for the class. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(super) fn iter_mro( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> MroIterator<'db> { - MroIterator::new(db, ClassLiteral::Static(self), specialization) - } - - /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of( - self, - db: &'db dyn Db, - specialization: Option>, - other: ClassType<'db>, - ) -> bool { - // `is_subclass_of` is checking the subtype relation, in which gradual types do not - // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. - self.iter_mro(db, specialization) - .contains(&ClassBase::Class(other)) - } - - /// Return `true` if this class constitutes a typed dict specification (inherits from - /// `typing.TypedDict`, either directly or indirectly). - #[salsa::tracked(cycle_initial=|_, _, _| false, heap_size=ruff_memory_usage::heap_size)] - pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { - if let Some(known) = self.known(db) { - return known.is_typed_dict_subclass(); - } - - self.iter_mro(db, None) - .any(|base| matches!(base, ClassBase::TypedDict)) - } - - /// Return `true` if this class is, or inherits from, a `NamedTuple` (inherits from - /// `typing.NamedTuple`, either directly or indirectly, including functional forms like - /// `NamedTuple("X", ...)`). - pub(crate) fn has_named_tuple_class_in_mro(self, db: &'db dyn Db) -> bool { - self.iter_mro(db, None) - .filter_map(ClassBase::into_class) - .any(|base| match base.class_literal(db) { - ClassLiteral::DynamicNamedTuple(_) => true, - ClassLiteral::Dynamic(_) => false, - ClassLiteral::Static(class) => class - .explicit_bases(db) - .contains(&Type::SpecialForm(SpecialFormType::NamedTuple)), - }) - } - - /// Compute `TypedDict` parameters dynamically based on MRO detection and AST parsing. - fn typed_dict_params(self, db: &'db dyn Db) -> Option { - if !self.is_typed_dict(db) { - return None; - } - - let module = parsed_module(db, self.file(db)).load(db); - let class_stmt = self.node(db, &module); - Some(typed_dict_params_from_class_def(class_stmt)) - } - - /// Returns dataclass params for this class, sourced from both dataclass params and dataclass - /// transform params - fn merged_dataclass_params( - self, - db: &'db dyn Db, - field_policy: CodeGeneratorKind<'db>, - ) -> (Option>, Option>) { - let dataclass_params = self.dataclass_params(db); - - let mut transformer_params = - if let CodeGeneratorKind::DataclassLike(Some(transformer_params)) = field_policy { - Some(DataclassParams::from_transformer_params( - db, - transformer_params, - )) - } else { - None - }; - - // Dataclass transformer flags can be overwritten using class arguments. - if let Some(transformer_params) = transformer_params.as_mut() { - if let Some(class_def) = self.definition(db).kind(db).as_class() { - let module = parsed_module(db, self.file(db)).load(db); - - if let Some(arguments) = &class_def.node(&module).arguments { - let mut flags = transformer_params.flags(db); - - for keyword in &arguments.keywords { - if let Some(arg_name) = &keyword.arg { - if let Some(is_set) = - keyword.value.as_boolean_literal_expr().map(|b| b.value) - { - for (flag_name, flag) in DATACLASS_FLAGS { - if arg_name.as_str() == *flag_name { - flags.set(*flag, is_set); - } - } - } - } - } - - *transformer_params = - DataclassParams::new(db, flags, transformer_params.field_specifiers(db)); - } - } - } - - (dataclass_params, transformer_params) - } - - /// Returns the effective frozen status of this class if it's a dataclass-like class. - /// - /// Returns `Some(true)` for a frozen dataclass-like class, `Some(false)` for a non-frozen one, - /// and `None` if the class is not a dataclass-like class, or if the dataclass is neither frozen - /// nor non-frozen. - pub(crate) fn is_frozen_dataclass(self, db: &'db dyn Db) -> Option { - // Check if this is a base-class-based transformer that has dataclass_transformer_params directly - // attached to it (because it is itself decorated with `@dataclass_transform`), or if this class - // has an explicit metaclass that is decorated with `@dataclass_transform`. - // - // In both cases, this signifies that this class is neither frozen nor non-frozen. - // - // See for details. - if self.dataclass_transformer_params(db).is_some() - || self - .try_metaclass(db) - .is_ok_and(|(_, info)| info.is_some_and(|i| i.from_explicit_metaclass)) - { - return None; - } - - if let field_policy @ CodeGeneratorKind::DataclassLike(_) = - CodeGeneratorKind::from_class(db, self.into(), None)? - { - // Otherwise, if this class is a dataclass-like class, determine its frozen status based on - // dataclass params and dataclass transformer params. - Some(self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN)) - } else { - None - } - } - - /// Checks if the given dataclass parameter flag is set for this class. - /// This checks both the `dataclass_params` and `transformer_params`. - fn has_dataclass_param( - self, - db: &'db dyn Db, - field_policy: CodeGeneratorKind<'db>, - param: DataclassFlags, - ) -> bool { - let (dataclass_params, transformer_params) = self.merged_dataclass_params(db, field_policy); - dataclass_params.is_some_and(|params| params.flags(db).contains(param)) - || transformer_params.is_some_and(|params| params.flags(db).contains(param)) - } - - /// Return the explicit `metaclass` of this class, if one is defined. - /// - /// ## Note - /// Only call this function from queries in the same file or your - /// query depends on the AST of another file (bad!). - fn explicit_metaclass(self, db: &'db dyn Db, module: &ParsedModuleRef) -> Option> { - let class_stmt = self.node(db, module); - let metaclass_node = &class_stmt - .arguments - .as_ref()? - .find_keyword("metaclass")? - .value; - - let class_definition = self.definition(db); - - Some(definition_expression_type( - db, - class_definition, - metaclass_node, - )) - } - - /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. - pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - self.try_metaclass(db) - .map(|(ty, _)| ty) - .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) - } - - /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. - #[salsa::tracked(cycle_initial=try_metaclass_cycle_initial, - heap_size=ruff_memory_usage::heap_size, - )] - pub(super) fn try_metaclass( - self, - db: &'db dyn Db, - ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { - tracing::trace!("StaticClassLiteral::try_metaclass: {}", self.name(db)); - - // Identify the class's own metaclass (or take the first base class's metaclass). - let mut base_classes = self.fully_static_explicit_bases(db).peekable(); - - if base_classes.peek().is_some() && self.inheritance_cycle(db).is_some() { - // We emit diagnostics for cyclic class definitions elsewhere. - // Avoid attempting to infer the metaclass if the class is cyclically defined. - return Ok((SubclassOfType::subclass_of_unknown(), None)); - } - - if self.try_mro(db, None).is_err_and(StaticMroError::is_cycle) { - return Ok((SubclassOfType::subclass_of_unknown(), None)); - } - - let module = parsed_module(db, self.file(db)).load(db); - - let explicit_metaclass = self.explicit_metaclass(db, &module); - - // Generic metaclasses parameterized by type variables are not supported. - // `metaclass=Meta[int]` is fine, but `metaclass=Meta[T]` is not. - // See: https://typing.python.org/en/latest/spec/generics.html#generic-metaclasses - if let Some(Type::GenericAlias(alias)) = explicit_metaclass { - let specialization_has_typevars = alias - .specialization(db) - .types(db) - .iter() - .any(|ty| ty.has_typevar_or_typevar_instance(db)); - if specialization_has_typevars { - return Err(MetaclassError { - kind: MetaclassErrorKind::GenericMetaclass, - }); - } - } - - let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { - (metaclass, self) - } else if let Some(base_class) = base_classes.next() { - // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking. - let base_class_literal = base_class - .static_class_literal(db) - .map(|(lit, _)| lit) - .unwrap_or(self); - (base_class.metaclass(db), base_class_literal) - } else { - (KnownClass::Type.to_class_literal(db), self) - }; - - let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { - MetaclassCandidate { - metaclass: metaclass_ty, - explicit_metaclass_of: class_metaclass_was_from, - } - } else { - let name = Type::string_literal(db, self.name(db)); - let bases = Type::heterogeneous_tuple(db, self.explicit_bases(db)); - let namespace = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); - - // TODO: Other keyword arguments? - let arguments = CallArguments::positional([name, bases, namespace]); - - let return_ty_result = match metaclass.try_call(db, &arguments) { - Ok(bindings) => Ok(bindings.return_type(db)), - - Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError { - kind: MetaclassErrorKind::NotCallable(bindings.callable_type()), - }), - - // TODO we should also check for binding errors that would indicate the metaclass - // does not accept the right arguments - Err(CallError(CallErrorKind::BindingError, bindings)) => { - Ok(bindings.return_type(db)) - } - - Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError { - kind: MetaclassErrorKind::PartlyNotCallable(metaclass), - }), - }; - - return return_ty_result.map(|ty| (ty.to_meta_type(db), None)); - }; - - // Reconcile all base classes' metaclasses with the candidate metaclass. - // - // See: - // - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass - // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 - for base_class in base_classes { - let metaclass = base_class.metaclass(db); - let Some(metaclass) = metaclass.to_class_type(db) else { - continue; - }; - // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking. - let base_class_literal = base_class - .static_class_literal(db) - .map(|(lit, _)| lit) - .unwrap_or(self); - if metaclass.is_subclass_of(db, candidate.metaclass) { - candidate = MetaclassCandidate { - metaclass, - explicit_metaclass_of: base_class_literal, - }; - continue; - } - if candidate.metaclass.is_subclass_of(db, metaclass) { - continue; - } - return Err(MetaclassError { - kind: MetaclassErrorKind::Conflict { - candidate1: candidate, - candidate2: MetaclassCandidate { - metaclass, - explicit_metaclass_of: base_class_literal, - }, - candidate1_is_base_class: explicit_metaclass.is_none(), - }, - }); - } - - let transform_info = candidate - .metaclass - .static_class_literal(db) - .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db)) - .map(|params| MetaclassTransformInfo { - params, - from_explicit_metaclass: candidate.explicit_metaclass_of == self, - }); - Ok((candidate.metaclass.into(), transform_info)) - } - - /// Returns the class member of this class named `name`. - /// - /// The member resolves to a member on the class itself or any of its proper superclasses. - /// - /// TODO: Should this be made private...? - pub(super) fn class_member( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { - match ty { - Type::Callable(callable_ty) => Type::Callable(CallableType::new( - db, - callable_ty.signatures(db), - CallableTypeKind::FunctionLike, - )), - Type::Union(union) => { - union.map(db, |element| into_function_like_callable(db, *element)) - } - Type::Intersection(intersection) => intersection - .map_positive(db, |element| into_function_like_callable(db, *element)), - _ => ty, - } - } - - let mut member = self.class_member_inner(db, None, name, policy); - - // We generally treat dunder attributes with `Callable` types as function-like callables. - // See `callables_as_descriptors.md` for more details. - if name.starts_with("__") && name.ends_with("__") { - member = member.map_type(|ty| into_function_like_callable(db, ty)); - } - - member - } - - fn class_member_inner( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) - } - - pub(super) fn class_member_from_mro( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - mro_iter: impl Iterator>, - ) -> PlaceAndQualifiers<'db> { - let result = MroLookup::new(db, mro_iter).class_member( - name, - policy, - self.inherited_generic_context(db), - self.is_known(db, KnownClass::Object), - ); - - match result { - ClassMemberResult::Done(result) => result.finalize(db), - - ClassMemberResult::TypedDict => KnownClass::TypedDictFallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) - .expect("Will return Some() when called on class literal") - .map_type(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - None, - ClassBase::is_typed_dict, - ), - }, - TypeContext::default(), - ) - }), - } - } - - /// Returns the inferred type of the class member named `name`. Only bound members - /// or those marked as `ClassVars` are considered. - /// - /// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope - /// directly. Use [`StaticClassLiteral::class_member`] if you require a method that will - /// traverse through the MRO until it finds the member. - pub(super) fn own_class_member( - self, - db: &'db dyn Db, - inherited_generic_context: Option>, - specialization: Option>, - name: &str, - ) -> Member<'db> { - // Check if this class is dataclass-like (either via @dataclass or via dataclass_transform) - if matches!( - CodeGeneratorKind::from_class(db, self.into(), specialization), - Some(CodeGeneratorKind::DataclassLike(_)) - ) { - if name == "__dataclass_fields__" { - // Make this class look like a subclass of the `DataClassInstance` protocol - return Member { - inner: Place::declared(KnownClass::Dict.to_specialized_instance( - db, - &[ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, &[Type::any()]), - ], - )) - .with_qualifiers(TypeQualifiers::CLASS_VAR), - }; - } else if name == "__dataclass_params__" { - // There is no typeshed class for this. For now, we model it as `Any`. - return Member { - inner: Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR), - }; - } - } - - if CodeGeneratorKind::NamedTuple.matches(db, self.into(), specialization) { - if let Some(field) = self - .own_fields(db, specialization, CodeGeneratorKind::NamedTuple) - .get(name) - { - let property_getter_signature = Signature::new( - Parameters::new( - db, - [Parameter::positional_only(Some(Name::new_static("self")))], - ), - field.declared_ty, - ); - let property_getter = Type::single_callable(db, property_getter_signature); - let property = PropertyInstanceType::new(db, Some(property_getter), None); - return Member::definitely_declared(Type::PropertyInstance(property)); - } - } - - let body_scope = self.body_scope(db); - let member = class_member(db, body_scope, name).map_type(|ty| { - // The `__new__` and `__init__` members of a non-specialized generic class are handled - // specially: they inherit the generic context of their class. That lets us treat them - // as generic functions when constructing the class, and infer the specialization of - // the class from the arguments that are passed in. - // - // We might decide to handle other class methods the same way, having them inherit the - // class's generic context, and performing type inference on calls to them to determine - // the specialization of the class. If we do that, we would update this to also apply - // to any method with a `@classmethod` decorator. (`__init__` would remain a special - // case, since it's an _instance_ method where we don't yet know the generic class's - // specialization.) - match (inherited_generic_context, ty, specialization, name) { - ( - Some(generic_context), - Type::FunctionLiteral(function), - Some(_), - "__new__" | "__init__", - ) => Type::FunctionLiteral( - function.with_inherited_generic_context(db, generic_context), - ), - _ => ty, - } - }); - - if member.is_undefined() { - if let Some(synthesized_member) = - self.own_synthesized_member(db, specialization, inherited_generic_context, name) - { - return Member::definitely_declared(synthesized_member); - } - // The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s. - return Self::implicit_attribute(db, body_scope, name, MethodDecorator::ClassMethod); - } - - // For dataclass-like classes, `KW_ONLY` sentinel fields are not real - // class attributes; they are markers used by the dataclass decorator to - // indicate that subsequent fields are keyword-only. Treat them as - // undefined so the MRO falls through to parent classes. - if member - .inner - .place - .unwidened_type() - .is_some_and(|ty| ty.is_instance_of(db, KnownClass::KwOnly)) - && CodeGeneratorKind::from_static_class(db, self, None) - .is_some_and(|policy| matches!(policy, CodeGeneratorKind::DataclassLike(_))) - { - return Member::unbound(); - } - - // For enum classes, `nonmember(value)` creates a non-member attribute. - // At runtime, the enum metaclass unwraps the value, so accessing the attribute - // returns the inner value, not the `nonmember` wrapper. - if let Some(ty) = member.inner.place.unwidened_type() { - if let Some(value_ty) = try_unwrap_nonmember_value(db, ty) { - if is_enum_class_by_inheritance(db, self) { - return Member::definitely_declared(value_ty); - } - } - } - - member - } - - /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or - /// a synthesized `__new__` method for a `NamedTuple`. - pub(super) fn own_synthesized_member( - self, - db: &'db dyn Db, - specialization: Option>, - inherited_generic_context: Option>, - name: &str, - ) -> Option> { - // Handle `@functools.total_ordering`: synthesize comparison methods - // for classes that have `@total_ordering` and define at least one - // ordering method. The decorator requires at least one of __lt__, - // __le__, __gt__, or __ge__ to be defined (either in this class or - // inherited from a superclass, excluding `object`). - // - // Only synthesize methods that are not already defined in the MRO. - // Note: We use direct scope lookups here to avoid infinite recursion - // through `own_class_member` -> `own_synthesized_member`. - if self.total_ordering(db) - && matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") - && !self - .iter_mro(db, specialization) - .filter_map(ClassBase::into_class) - .filter_map(|class| class.static_class_literal(db)) - .filter(|(class, _)| !class.is_known(db, KnownClass::Object)) - .any(|(class, _)| { - class_member(db, class.body_scope(db), name) - .ignore_possibly_undefined() - .is_some() - }) - && self.has_ordering_method_in_mro(db, specialization) - && let Some(root_method_ty) = self.total_ordering_root_method(db, specialization) - && let Some(callables) = root_method_ty.try_upcast_to_callable(db) - { - let bool_ty = KnownClass::Bool.to_instance(db); - let synthesized_callables = callables.map(|callable| { - let signatures = CallableSignature::from_overloads( - callable.signatures(db).iter().map(|signature| { - // The generated methods return a union of the root method's return type - // and `bool`. This is because `@total_ordering` synthesizes methods like: - // def __gt__(self, other): return not (self == other or self < other) - // If `__lt__` returns `int`, then `__gt__` could return `int | bool`. - let return_ty = - UnionType::from_two_elements(db, signature.return_ty, bool_ty); - Signature::new_generic( - signature.generic_context, - signature.parameters().clone(), - return_ty, - ) - }), - ); - CallableType::new(db, signatures, CallableTypeKind::FunctionLike) - }); - - return Some(synthesized_callables.into_type(db)); - } - - let field_policy = CodeGeneratorKind::from_class(db, self.into(), specialization)?; - - let instance_ty = - Type::instance(db, self.apply_optional_specialization(db, specialization)); - - let signature_from_fields = |mut parameters: Vec<_>, return_ty: Type<'db>| { - for (field_name, field) in self.fields(db, specialization, field_policy) { - let (init, mut default_ty, kw_only, alias) = match &field.kind { - FieldKind::NamedTuple { default_ty } => (true, *default_ty, None, None), - FieldKind::Dataclass { - init, - default_ty, - kw_only, - alias, - .. - } => (*init, *default_ty, *kw_only, alias.as_ref()), - FieldKind::TypedDict { .. } => continue, - }; - let mut field_ty = field.declared_ty; - - if name == "__init__" && !init { - // Skip fields with `init=False` - continue; - } - - if field.is_kw_only_sentinel(db) { - // Attributes annotated with `dataclass.KW_ONLY` are not present in the synthesized - // `__init__` method; they are used to indicate that the following parameters are - // keyword-only. - continue; - } - - let dunder_set = field_ty.class_member(db, "__set__".into()); - if let Place::Defined(DefinedPlace { - ty: dunder_set, - definedness: Definedness::AlwaysDefined, - .. - }) = dunder_set.place - { - // The descriptor handling below is guarded by this not-dynamic check, because - // dynamic types like `Any` are valid (data) descriptors: since they have all - // possible attributes, they also have a (callable) `__set__` method. The - // problem is that we can't determine the type of the value parameter this way. - // Instead, we want to use the dynamic type itself in this case, so we skip the - // special descriptor handling. - if !dunder_set.is_dynamic() { - // This type of this attribute is a data descriptor. Instead of overwriting the - // descriptor attribute, data-classes will (implicitly) call the `__set__` method - // of the descriptor. This means that the synthesized `__init__` parameter for - // this attribute is determined by possible `value` parameter types with which - // the `__set__` method can be called. - // - // We union parameter types across overloads of a single callable, intersect - // callable bindings inside an intersection element, and union outer elements. - field_ty = dunder_set.bindings(db).map_types(db, |binding| { - let mut value_types = UnionBuilder::new(db); - let mut has_value_type = false; - for overload in binding { - if let Some(value_param) = - overload.signature.parameters().get_positional(2) - { - value_types = value_types.add(value_param.annotated_type()); - has_value_type = true; - } else if overload.signature.parameters().is_gradual() { - value_types = value_types.add(Type::unknown()); - has_value_type = true; - } - } - has_value_type.then(|| value_types.build()) - }); - - // The default value of the attribute is *not* determined by the right hand side - // of the class-body assignment. Instead, the runtime invokes `__get__` on the - // descriptor, as if it had been called on the class itself, i.e. it passes `None` - // for the `instance` argument. - - if let Some(ref mut default_ty) = default_ty { - *default_ty = default_ty - .try_call_dunder_get(db, None, Type::from(self)) - .map(|(return_ty, _)| return_ty) - .unwrap_or_else(Type::unknown); - } - } - } - - let is_kw_only = - matches!(name, "__replace__" | "_replace") || kw_only.unwrap_or(false); - - // Use the alias name if provided, otherwise use the field name - let parameter_name = - Name::new(alias.map(|alias| &**alias).unwrap_or(&**field_name)); - - let mut parameter = if is_kw_only { - Parameter::keyword_only(parameter_name) - } else { - Parameter::positional_or_keyword(parameter_name) - } - .with_annotated_type(field_ty); - - parameter = if matches!(name, "__replace__" | "_replace") { - // When replacing, we know there is a default value for the field - // (the value that is currently assigned to the field) - // assume this to be the declared type of the field - parameter.with_default_type(field_ty) - } else { - parameter.with_optional_default_type(default_ty) - }; - - parameters.push(parameter); - } - - // In the event that we have a mix of keyword-only and positional parameters, we need to sort them - // so that the keyword-only parameters appear after positional parameters. - parameters.sort_by_key(Parameter::is_keyword_only); - - let signature = match name { - "__new__" | "__init__" => Signature::new_generic( - inherited_generic_context.or_else(|| self.inherited_generic_context(db)), - Parameters::new(db, parameters), - return_ty, - ), - _ => Signature::new(Parameters::new(db, parameters), return_ty), - }; - Some(Type::function_like_callable(db, signature)) - }; - - match (field_policy, name) { - (CodeGeneratorKind::DataclassLike(_), "__init__") => { - if !self.has_dataclass_param(db, field_policy, DataclassFlags::INIT) { - return None; - } - - let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) - // TODO: could be `Self`. - .with_annotated_type(instance_ty); - signature_from_fields(vec![self_parameter], Type::none(db)) - } - ( - CodeGeneratorKind::NamedTuple, - "__new__" | "__init__" | "_replace" | "__replace__" | "_fields", - ) if self.namedtuple_base_has_unknown_fields(db) => { - // When the namedtuple base has unknown fields, fall back to NamedTupleFallback - // which has generic signatures that accept any arguments. - KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .as_static()? - .own_class_member(db, inherited_generic_context, None, name) - .ignore_possibly_undefined() - .map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: instance_ty, - }, - TypeContext::default(), - ) - }) - } - ( - CodeGeneratorKind::NamedTuple, - "__new__" | "_replace" | "__replace__" | "_fields" | "__slots__", - ) => { - let fields = self.fields(db, specialization, field_policy); - let fields_iter = fields.iter().map(|(name, field)| { - let default_ty = match &field.kind { - FieldKind::NamedTuple { default_ty } => *default_ty, - _ => None, - }; - NamedTupleField { - name: name.clone(), - ty: field.declared_ty, - default: default_ty, - } - }); - synthesize_namedtuple_class_member( - db, - name, - instance_ty, - fields_iter, - specialization.map(|s| s.generic_context(db)), - ) - } - (CodeGeneratorKind::DataclassLike(_), "__lt__" | "__le__" | "__gt__" | "__ge__") => { - if !self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) { - return None; - } - - let signature = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_or_keyword(Name::new_static("self")) - // TODO: could be `Self`. - .with_annotated_type(instance_ty), - Parameter::positional_or_keyword(Name::new_static("other")) - // TODO: could be `Self`. - .with_annotated_type(instance_ty), - ], - ), - KnownClass::Bool.to_instance(db), - ); - - Some(Type::function_like_callable(db, signature)) - } - (CodeGeneratorKind::DataclassLike(_), "__hash__") => { - let unsafe_hash = - self.has_dataclass_param(db, field_policy, DataclassFlags::UNSAFE_HASH); - let frozen = self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN); - let eq = self.has_dataclass_param(db, field_policy, DataclassFlags::EQ); - - if unsafe_hash || (frozen && eq) { - let signature = Signature::new( - Parameters::new( - db, - [Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty)], - ), - KnownClass::Int.to_instance(db), - ); - - Some(Type::function_like_callable(db, signature)) - } else if eq && !frozen { - Some(Type::none(db)) - } else { - // No `__hash__` is generated, fall back to `object.__hash__` - None - } - } - (CodeGeneratorKind::DataclassLike(_), "__match_args__") - if Program::get(db).python_version(db) >= PythonVersion::PY310 => - { - if !self.has_dataclass_param(db, field_policy, DataclassFlags::MATCH_ARGS) { - return None; - } - - let kw_only_default = - self.has_dataclass_param(db, field_policy, DataclassFlags::KW_ONLY); - - let fields = self.fields(db, specialization, field_policy); - let match_args = fields - .iter() - .filter(|(_, field)| { - if let FieldKind::Dataclass { init, kw_only, .. } = &field.kind { - *init && !kw_only.unwrap_or(kw_only_default) - } else { - false - } - }) - .map(|(name, _)| Type::string_literal(db, name)); - Some(Type::heterogeneous_tuple(db, match_args)) - } - (CodeGeneratorKind::DataclassLike(_), "__weakref__") - if Program::get(db).python_version(db) >= PythonVersion::PY311 => - { - if !self.has_dataclass_param(db, field_policy, DataclassFlags::WEAKREF_SLOT) - || !self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) - { - return None; - } - - // This could probably be `weakref | None`, but it does not seem important enough to - // model it precisely. - Some(UnionType::from_two_elements( - db, - Type::any(), - Type::none(db), - )) - } - (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { - KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .as_static()? - .own_class_member(db, self.inherited_generic_context(db), None, name) - .ignore_possibly_undefined() - .map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - specialization, - |base| { - base.into_class() - .is_some_and(|c| c.is_known(db, KnownClass::Tuple)) - }, - ), - }, - TypeContext::default(), - ) - }) - } - (CodeGeneratorKind::DataclassLike(_), "__replace__") - if Program::get(db).python_version(db) >= PythonVersion::PY313 => - { - let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty); - - signature_from_fields(vec![self_parameter], instance_ty) - } - (CodeGeneratorKind::DataclassLike(_), "__setattr__") => { - if self.is_frozen_dataclass(db) == Some(true) { - let signature = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty), - Parameter::positional_or_keyword(Name::new_static("name")), - Parameter::positional_or_keyword(Name::new_static("value")), - ], - ), - Type::Never, - ); - - return Some(Type::function_like_callable(db, signature)); - } - None - } - (CodeGeneratorKind::DataclassLike(_), "__slots__") - if Program::get(db).python_version(db) >= PythonVersion::PY310 => - { - self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) - .then(|| { - let fields = self.fields(db, specialization, field_policy); - let slots = fields.keys().map(|name| Type::string_literal(db, name)); - Type::heterogeneous_tuple(db, slots) - }) - } - (CodeGeneratorKind::TypedDict, "__setitem__") => { - let fields = self.fields(db, specialization, field_policy); - - // Add (key type, value type) overloads for all TypedDict items ("fields") that are not read-only: - - let mut writeable_fields = fields - .iter() - .filter(|(_, field)| !field.is_read_only()) - .peekable(); - - if writeable_fields.peek().is_none() { - // If there are no writeable fields, synthesize a `__setitem__` that takes - // a `key` of type `Never` to signal that no keys are accepted. This leads - // to slightly more user-friendly error messages compared to returning an - // empty overload set. - return Some(Type::Callable(CallableType::new( - db, - CallableSignature::single(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(Type::Never), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::any()), - ], - ), - Type::none(db), - )), - CallableTypeKind::FunctionLike, - ))); - } - - let overloads = writeable_fields.map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(field.declared_ty), - ], - ), - Type::none(db), - ) - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "__getitem__") => { - let fields = self.fields(db, specialization, field_policy); - - // Add (key -> value type) overloads for all TypedDict items ("fields"): - let overloads = fields.iter().map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ], - ), - field.declared_ty, - ) - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "__delitem__") => { - let fields = self.fields(db, specialization, field_policy); - - // Only non-required fields can be deleted. Required fields cannot be deleted - // because that would violate the TypedDict's structural type. - let mut deletable_fields = fields - .iter() - .filter(|(_, field)| !field.is_required()) - .peekable(); - - if deletable_fields.peek().is_none() { - // If there are no deletable fields (all fields are required), synthesize a - // `__delitem__` that takes a `key` of type `Never` to signal that no keys - // can be deleted. - return Some(Type::Callable(CallableType::new( - db, - CallableSignature::single(Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(Type::Never), - ], - ), - Type::none(db), - )), - CallableTypeKind::FunctionLike, - ))); - } - - // Otherwise, add overloads for all deletable fields. - let overloads = deletable_fields.map(|(name, _field)| { - let key_type = Type::string_literal(db, name); - - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ], - ), - Type::none(db), - ) - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "get") => { - let overloads = self - .fields(db, specialization, field_policy) - .iter() - .flat_map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - // For a required key, `.get()` always returns the value type. For a non-required key, - // `.get()` returns the union of the value type and the type of the default argument - // (which defaults to `None`). - - // TODO: For now, we use two overloads here. They can be merged into a single function - // once the generics solver takes default arguments into account. - - let get_sig = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ], - ), - if field.is_required() { - field.declared_ty - } else { - UnionType::from_two_elements(db, field.declared_ty, Type::none(db)) - }, - ); - - let t_default = BoundTypeVarInstance::synthetic( - db, - Name::new_static("T"), - TypeVarVariance::Covariant, - ); - - let get_with_default_sig = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ], - ), - if field.is_required() { - field.declared_ty - } else { - UnionType::from_two_elements( - db, - field.declared_ty, - Type::TypeVar(t_default), - ) - }, - ); - - [get_sig, get_with_default_sig] - }) - // Fallback overloads for unknown keys - .chain(std::iter::once({ - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), - ], - ), - UnionType::from_two_elements(db, Type::unknown(), Type::none(db)), - ) - })) - .chain(std::iter::once({ - let t_default = BoundTypeVarInstance::synthetic( - db, - Name::new_static("T"), - TypeVarVariance::Covariant, - ); - - Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ], - ), - UnionType::from_two_elements( - db, - Type::unknown(), - Type::TypeVar(t_default), - ), - ) - })); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "pop") => { - let fields = self.fields(db, specialization, field_policy); - let overloads = fields - .iter() - .filter(|(_, field)| { - // Only synthesize `pop` for fields that are not required. - !field.is_required() - }) - .flat_map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - // TODO: Similar to above: consider merging these two overloads into one - - // `.pop()` without default - let pop_sig = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ], - ), - field.declared_ty, - ); - - // `.pop()` with a default value - let t_default = BoundTypeVarInstance::synthetic( - db, - Name::new_static("T"), - TypeVarVariance::Covariant, - ); - - let pop_with_default_sig = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ], - ), - UnionType::from_two_elements( - db, - field.declared_ty, - Type::TypeVar(t_default), - ), - ); - - [pop_sig, pop_with_default_sig] - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "setdefault") => { - let fields = self.fields(db, specialization, field_policy); - let overloads = fields.iter().map(|(name, field)| { - let key_type = Type::string_literal(db, name); - - // `setdefault` always returns the field type - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(field.declared_ty), - ], - ), - field.declared_ty, - ) - }); - - Some(Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(overloads), - CallableTypeKind::FunctionLike, - ))) - } - (CodeGeneratorKind::TypedDict, "update") => { - // TODO: synthesize a set of overloads with precise types - let signature = Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::variadic(Name::new_static("args")), - Parameter::keyword_variadic(Name::new_static("kwargs")), - ], - ), - Type::none(db), - ); - - Some(Type::function_like_callable(db, signature)) - } - _ => None, - } - } - - /// Member lookup for classes that inherit from `typing.TypedDict`. - /// - /// This is implemented as a separate method because the item definitions on a `TypedDict`-based - /// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`, - /// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`. - pub(crate) fn typed_dict_member( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - if let Some(member) = self.own_synthesized_member(db, specialization, None, name) { - Place::bound(member).into() - } else { - KnownClass::TypedDictFallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) - .expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal") - .map_type(|ty| - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - specialization, - ClassBase::is_typed_dict - ) - }, - TypeContext::default(), - ) - ) - } - } - - /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. - /// - /// See [`StaticClassLiteral::own_fields`] for more details. - #[salsa::tracked( - returns(ref), - cycle_initial=|_, _, _, _, _| FxIndexMap::default(), - heap_size=get_size2::GetSize::get_heap_size)] - pub(crate) fn fields( - self, - db: &'db dyn Db, - specialization: Option>, - field_policy: CodeGeneratorKind<'db>, - ) -> FxIndexMap> { - if field_policy == CodeGeneratorKind::NamedTuple { - // NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the - // fields of this class only. - return self.own_fields(db, specialization, field_policy); - } - - let matching_classes_in_mro: Vec<(StaticClassLiteral<'db>, Option>)> = - self.iter_mro(db, specialization) - .filter_map(|superclass| { - let class = superclass.into_class()?; - // Dynamic classes don't have fields (no class body). - let (class_literal, specialization) = class.static_class_literal(db)?; - if field_policy.matches(db, class_literal.into(), specialization) { - Some((class_literal, specialization)) - } else { - None - } - }) - // We need to collect into a `Vec` here because we iterate the MRO in reverse order - .collect(); - - matching_classes_in_mro - .into_iter() - .rev() - .flat_map(|(class, specialization)| class.own_fields(db, specialization, field_policy)) - // KW_ONLY sentinels are markers, not real fields. Exclude them so - // they cannot shadow an inherited field with the same name. - .filter(|(_, field)| !field.is_kw_only_sentinel(db)) - // We collect into a FxOrderMap here to deduplicate attributes - .collect() - } - - pub(crate) fn validate_members(self, context: &InferContext<'db, '_>) { - let db = context.db(); - let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { - return; - }; - let class_body_scope = self.body_scope(db); - let table = place_table(db, class_body_scope); - let use_def = use_def_map(db, class_body_scope); - for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations.clone()); - let attr = result.ignore_conflicting_declarations(); - let symbol = table.symbol(symbol_id); - let name = symbol.name(); - - let Some(Type::FunctionLiteral(literal)) = attr.place.ignore_possibly_undefined() - else { - continue; - }; - - match name.as_str() { - "__setattr__" | "__delattr__" => { - if let CodeGeneratorKind::DataclassLike(_) = field_policy - && self.is_frozen_dataclass(db) == Some(true) - { - if let Some(builder) = context.report_lint( - &INVALID_DATACLASS_OVERRIDE, - literal.node(db, context.file(), context.module()), - ) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot overwrite attribute `{}` in frozen dataclass `{}`", - name, - self.name(db) - )); - diagnostic.info(name); - } - } - } - "__lt__" | "__le__" | "__gt__" | "__ge__" => { - if let CodeGeneratorKind::DataclassLike(_) = field_policy - && self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) - { - if let Some(builder) = context.report_lint( - &INVALID_DATACLASS_OVERRIDE, - literal.node(db, context.file(), context.module()), - ) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot overwrite attribute `{}` in dataclass `{}` with `order=True`", - name, - self.name(db) - )); - diagnostic.info(name); - } - } - } - _ => {} - } - } - } - - /// Returns a map of all annotated attributes defined in the body of this class. - /// This extends the `__annotations__` attribute at runtime by also including default values - /// and computed field properties. - /// - /// For a class body like - /// ```py - /// @dataclass(kw_only=True) - /// class C: - /// x: int - /// y: str = "hello" - /// z: float = field(kw_only=False, default=1.0) - /// ``` - /// we return a map `{"x": Field, "y": Field, "z": Field}` where each `Field` contains - /// the annotated type, default value (if any), and field properties. - /// - /// **Important**: The returned `Field` objects represent our full understanding of the fields, - /// including properties inherited from class-level dataclass parameters (like `kw_only=True`) - /// and dataclass-transform parameters (like `kw_only_default=True`). They do not represent - /// only what is explicitly specified in each field definition. - pub(super) fn own_fields( - self, - db: &'db dyn Db, - specialization: Option>, - field_policy: CodeGeneratorKind, - ) -> FxIndexMap> { - let mut attributes = FxIndexMap::default(); - - let class_body_scope = self.body_scope(db); - let table = place_table(db, class_body_scope); - - let use_def = use_def_map(db, class_body_scope); - - let typed_dict_params = self.typed_dict_params(db); - let mut kw_only_sentinel_field_seen = false; - - for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - // Here, we exclude all declarations that are not annotated assignments. We need this because - // things like function definitions and nested classes would otherwise be considered dataclass - // fields. The check is too broad in the sense that it also excludes (weird) constructs where - // a symbol would have multiple declarations, one of which is an annotated assignment. If we - // want to improve this, we could instead pass a definition-kind filter to the use-def map - // query, or to the `symbol_from_declarations` call below. Doing so would potentially require - // us to generate a union of `__init__` methods. - if !declarations - .clone() - .all(|DeclarationWithConstraint { declaration, .. }| { - declaration.is_undefined_or(|declaration| { - matches!( - declaration.kind(db), - DefinitionKind::AnnotatedAssignment(..) - ) - }) - }) - { - continue; - } - - let symbol = table.symbol(symbol_id); - - let result = place_from_declarations(db, declarations.clone()); - let first_declaration = result.first_declaration; - let attr = result.ignore_conflicting_declarations(); - if attr.is_class_var() { - continue; - } - - if let Some(attr_ty) = attr.place.ignore_possibly_undefined() { - let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let mut default_ty = place_from_bindings(db, bindings) - .place - .ignore_possibly_undefined(); - - default_ty = - default_ty.map(|ty| ty.apply_optional_specialization(db, specialization)); - - let mut init = true; - let mut kw_only = None; - let mut alias = None; - if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty { - default_ty = field.default_type(db); - if self - .dataclass_params(db) - .map(|params| params.field_specifiers(db).is_empty()) - .unwrap_or(false) - { - // This happens when constructing a `dataclass` with a `dataclass_transform` - // without defining the `field_specifiers`, meaning it should ignore - // `dataclasses.field` and `dataclasses.Field`. - } else { - init = field.init(db); - kw_only = field.kw_only(db); - alias = field.alias(db); - } - } - - let kind = match field_policy { - CodeGeneratorKind::NamedTuple => FieldKind::NamedTuple { default_ty }, - CodeGeneratorKind::DataclassLike(_) => FieldKind::Dataclass { - default_ty, - init_only: attr.is_init_var(), - init, - kw_only, - alias, - }, - CodeGeneratorKind::TypedDict => { - let is_required = if attr.is_required() { - // Explicit Required[T] annotation - always required - true - } else if attr.is_not_required() { - // Explicit NotRequired[T] annotation - never required - false - } else { - // No explicit qualifier - use class default (`total` parameter) - typed_dict_params - .expect("TypedDictParams should be available for CodeGeneratorKind::TypedDict") - .contains(TypedDictParams::TOTAL) - }; - - FieldKind::TypedDict { - is_required, - is_read_only: attr.is_read_only(), - } - } - }; - - let mut field = Field { - declared_ty: attr_ty.apply_optional_specialization(db, specialization), - kind, - first_declaration, - }; - - // Check if this is a KW_ONLY sentinel and mark subsequent fields as keyword-only - if field.is_kw_only_sentinel(db) { - kw_only_sentinel_field_seen = true; - } - - // If no explicit kw_only setting and we've seen KW_ONLY sentinel, mark as keyword-only - if kw_only_sentinel_field_seen { - if let FieldKind::Dataclass { - kw_only: ref mut kw @ None, - .. - } = field.kind - { - *kw = Some(true); - } - } - - // Resolve the kw_only to the class-level default. This ensures that when fields - // are inherited by child classes, they use their defining class's kw_only default. - if let FieldKind::Dataclass { - kw_only: ref mut kw @ None, - .. - } = field.kind - { - let class_kw_only_default = self - .dataclass_params(db) - .is_some_and(|params| params.flags(db).contains(DataclassFlags::KW_ONLY)) - // TODO this next part should not be necessary, if we were properly - // initializing `dataclass_params` from the dataclass-transform params, for - // metaclass and base-class-based dataclass-transformers. - || matches!( - field_policy, - CodeGeneratorKind::DataclassLike(Some(transformer_params)) - if transformer_params.flags(db).contains(DataclassTransformerFlags::KW_ONLY_DEFAULT) - ); - *kw = Some(class_kw_only_default); - } - - attributes.insert(symbol.name().clone(), field); - } - } - - attributes - } - - /// Look up an instance attribute (available in `__dict__`) of the given name. - /// - /// See [`Type::instance_member`] for more details. - pub(super) fn instance_member( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - ) -> PlaceAndQualifiers<'db> { - if self.is_typed_dict(db) { - return Place::Undefined.into(); - } - - match MroLookup::new(db, self.iter_mro(db, specialization)).instance_member(name) { - InstanceMemberResult::Done(result) => result, - InstanceMemberResult::TypedDict => KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) - .map_type(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: Type::instance(db, self.unknown_specialization(db)), - }, - TypeContext::default(), - ) - }), - } - } - - /// Tries to find declarations/bindings of an attribute named `name` that are only - /// "implicitly" defined (`self.x = …`, `cls.x = …`) in a method of the class that - /// corresponds to `class_body_scope`. The `target_method_decorator` parameter is - /// used to skip methods that do not have the expected decorator. - fn implicit_attribute( - db: &'db dyn Db, - class_body_scope: ScopeId<'db>, - name: &str, - target_method_decorator: MethodDecorator, - ) -> Member<'db> { - Self::implicit_attribute_inner( - db, - class_body_scope, - name.to_string(), - target_method_decorator, - ) - } - - #[salsa::tracked( - cycle_fn=implicit_attribute_cycle_recover, - cycle_initial=implicit_attribute_initial, - heap_size=ruff_memory_usage::heap_size, - )] - pub(super) fn implicit_attribute_inner( - db: &'db dyn Db, - class_body_scope: ScopeId<'db>, - name: String, - target_method_decorator: MethodDecorator, - ) -> Member<'db> { - // If we do not see any declarations of an attribute, neither in the class body nor in - // any method, we build a union of `Unknown` with the inferred types of all bindings of - // that attribute. We include `Unknown` in that union to account for the fact that the - // attribute might be externally modified. - let mut union_of_inferred_types = UnionBuilder::new(db); - let mut qualifiers = TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE; - - let mut is_attribute_bound = false; - - let file = class_body_scope.file(db); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); - let class_map = use_def_map(db, class_body_scope); - let class_table = place_table(db, class_body_scope); - let is_valid_scope = |method_scope: &Scope| { - let Some(method_def) = method_scope.node().as_function() else { - return true; - }; - - // Check the decorators directly on the AST node to determine if this method - // is a classmethod or staticmethod. This is more reliable than checking the - // final evaluated type, which may be wrapped by other decorators like @cache. - let function_node = method_def.node(&module); - let definition = index.expect_single_definition(method_def); - - let mut is_classmethod = false; - let mut is_staticmethod = false; - - for decorator in &function_node.decorator_list { - let decorator_ty = - definition_expression_type(db, definition, &decorator.expression); - if let Type::ClassLiteral(class) = decorator_ty { - match class.known(db) { - Some(KnownClass::Classmethod) => is_classmethod = true, - Some(KnownClass::Staticmethod) => is_staticmethod = true, - _ => {} - } - } - } - - // Also check for implicit classmethods/staticmethods based on method name - let method_name = function_node.name.as_str(); - if is_implicit_classmethod(method_name) { - is_classmethod = true; - } - if is_implicit_staticmethod(method_name) { - is_staticmethod = true; - } - - match target_method_decorator { - MethodDecorator::None => !is_classmethod && !is_staticmethod, - MethodDecorator::ClassMethod => is_classmethod, - MethodDecorator::StaticMethod => is_staticmethod, - } - }; - - // First check declarations - for (attribute_declarations, method_scope_id) in - attribute_declarations(db, class_body_scope, &name) - { - let method_scope = index.scope(method_scope_id); - if !is_valid_scope(method_scope) { - continue; - } - - for attribute_declaration in attribute_declarations { - let DefinitionState::Defined(declaration) = attribute_declaration.declaration - else { - continue; - }; - - let DefinitionKind::AnnotatedAssignment(assignment) = declaration.kind(db) else { - continue; - }; - - // We found an annotated assignment of one of the following forms (using 'self' in these - // examples, but we support arbitrary names for the first parameters of methods): - // - // self.name: - // self.name: = … - - let annotation = declaration_type(db, declaration); - let annotation = Place::declared(annotation.inner).with_qualifiers( - annotation.qualifiers | TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE, - ); - - if let Some(all_qualifiers) = annotation.is_bare_final() { - if let Some(value) = assignment.value(&module) { - // If we see an annotated assignment with a bare `Final` as in - // `self.SOME_CONSTANT: Final = 1`, infer the type from the value - // on the right-hand side. - - let inferred_ty = infer_expression_type( - db, - index.expression(value), - TypeContext::default(), - ); - return Member { - inner: Place::bound(inferred_ty).with_qualifiers(all_qualifiers), - }; - } - - // If there is no right-hand side, just record that we saw a `Final` qualifier - qualifiers |= all_qualifiers; - continue; - } - - return Member { inner: annotation }; - } - } - - if !qualifiers.contains(TypeQualifiers::FINAL) { - union_of_inferred_types = union_of_inferred_types.add(Type::unknown()); - } - - for (attribute_assignments, attribute_binding_scope_id) in - attribute_assignments(db, class_body_scope, &name) - { - let binding_scope = index.scope(attribute_binding_scope_id); - if !is_valid_scope(binding_scope) { - continue; - } - - let scope_for_reachability_analysis = { - if binding_scope.node().as_function().is_some() { - binding_scope - } else if binding_scope.is_eager() { - let mut eager_scope_parent = binding_scope; - while eager_scope_parent.is_eager() - && let Some(parent) = eager_scope_parent.parent() - { - eager_scope_parent = index.scope(parent); - } - eager_scope_parent - } else { - binding_scope - } - }; - - // The attribute assignment inherits the reachability of the method which contains it - let is_method_reachable = - if let Some(method_def) = scope_for_reachability_analysis.node().as_function() { - let method = index.expect_single_definition(method_def); - let method_place = class_table - .symbol_id(&method_def.node(&module).name) - .unwrap(); - class_map - .reachable_symbol_bindings(method_place) - .find_map(|bind| { - (bind.binding.is_defined_and(|def| def == method)) - .then(|| class_map.binding_reachability(db, &bind)) - }) - .unwrap_or(Truthiness::AlwaysFalse) - } else { - Truthiness::AlwaysFalse - }; - if is_method_reachable.is_always_false() { - continue; - } - - for attribute_assignment in attribute_assignments { - if let DefinitionState::Undefined = attribute_assignment.binding { - continue; - } - - let DefinitionState::Defined(binding) = attribute_assignment.binding else { - continue; - }; - - if !is_method_reachable.is_always_false() { - is_attribute_bound = true; - } - - match binding.kind(db) { - DefinitionKind::AnnotatedAssignment(_) => { - // Annotated assignments were handled above. This branch is not - // unreachable (because of the `continue` above), but there is - // nothing to do here. - } - DefinitionKind::Assignment(assign) => { - match assign.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // .., self.name, .. = - // (.., self.name, ..) = - // [.., self.name, ..] = - - let unpacked = infer_unpack_types(db, unpack); - - let inferred_ty = unpacked.expression_type(assign.target(&module)); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::Single => { - // We found an un-annotated attribute assignment of the form: - // - // self.name = - - let inferred_ty = infer_expression_type( - db, - index.expression(assign.value(&module)), - TypeContext::default(), - ); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::For(for_stmt) => { - match for_stmt.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // for .., self.name, .. in : - - let unpacked = infer_unpack_types(db, unpack); - let inferred_ty = - unpacked.expression_type(for_stmt.target(&module)); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::Single => { - // We found an attribute assignment like: - // - // for self.name in : - - let iterable_ty = infer_expression_type( - db, - index.expression(for_stmt.iterable(&module)), - TypeContext::default(), - ); - // TODO: Potential diagnostics resulting from the iterable are currently not reported. - let inferred_ty = - iterable_ty.iterate(db).homogeneous_element_type(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::WithItem(with_item) => { - match with_item.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // with as .., self.name, ..: - - let unpacked = infer_unpack_types(db, unpack); - let inferred_ty = - unpacked.expression_type(with_item.target(&module)); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::Single => { - // We found an attribute assignment like: - // - // with as self.name: - - let context_ty = infer_expression_type( - db, - index.expression(with_item.context_expr(&module)), - TypeContext::default(), - ); - let inferred_ty = if with_item.is_async() { - context_ty.aenter(db) - } else { - context_ty.enter(db) - }; - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::Comprehension(comprehension) => { - match comprehension.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // [... for .., self.name, .. in ] - - let unpacked = infer_unpack_types(db, unpack); - - let inferred_ty = - unpacked.expression_type(comprehension.target(&module)); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::Single => { - // We found an attribute assignment like: - // - // [... for self.name in ] - - let iterable_ty = infer_expression_type( - db, - index.expression(comprehension.iterable(&module)), - TypeContext::default(), - ); - // TODO: Potential diagnostics resulting from the iterable are currently not reported. - let inferred_ty = - iterable_ty.iterate(db).homogeneous_element_type(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::AugmentedAssignment(_) => { - // TODO: - } - DefinitionKind::NamedExpression(_) => { - // A named expression whose target is an attribute is syntactically prohibited - } - _ => {} - } - } - } - - Member { - inner: if is_attribute_bound { - Place::bound(union_of_inferred_types.build()).with_qualifiers(qualifiers) - } else { - Place::Undefined.with_qualifiers(qualifiers) - }, - } - } - - /// A helper function for `instance_member` that looks up the `name` attribute only on - /// this class, not on its superclasses. - fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - // TODO: There are many things that are not yet implemented here: - // - `typing.Final` - // - Proper diagnostics - - let body_scope = self.body_scope(db); - let table = place_table(db, body_scope); - - if let Some(symbol_id) = table.symbol_id(name) { - let use_def = use_def_map(db, body_scope); - - let declarations = use_def.end_of_scope_symbol_declarations(symbol_id); - let declared_and_qualifiers = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); - - match declared_and_qualifiers { - PlaceAndQualifiers { - place: - mut declared @ Place::Defined(DefinedPlace { - ty: declared_ty, - definedness: declaredness, - .. - }), - qualifiers, - } => { - // For the purpose of finding instance attributes, ignore `ClassVar` - // declarations: - if qualifiers.contains(TypeQualifiers::CLASS_VAR) { - declared = Place::Undefined; - } - - if qualifiers.contains(TypeQualifiers::INIT_VAR) { - // We ignore `InitVar` declarations on the class body, unless that attribute is overwritten - // by an implicit assignment in a method - if Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) - .is_undefined() - { - return Member::unbound(); - } - } - - // `KW_ONLY` sentinels are markers, not real instance attributes. - if declared_ty.is_instance_of(db, KnownClass::KwOnly) - && CodeGeneratorKind::from_static_class(db, self, None).is_some_and( - |policy| matches!(policy, CodeGeneratorKind::DataclassLike(_)), - ) - { - return Member::unbound(); - } - - // The attribute is declared in the class body. - - let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let inferred = place_from_bindings(db, bindings).place; - let has_binding = !inferred.is_undefined(); - - if has_binding { - // The attribute is declared and bound in the class body. - - if let Some(implicit_ty) = - Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) - .ignore_possibly_undefined() - { - if declaredness == Definedness::AlwaysDefined { - // If a symbol is definitely declared, and we see - // attribute assignments in methods of the class, - // we trust the declared type. - Member { - inner: declared.with_qualifiers(qualifiers), - } - } else { - Member { - inner: Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements( - db, - declared_ty, - implicit_ty, - ), - origin: TypeOrigin::Declared, - definedness: declaredness, - widening: Widening::None, - }) - .with_qualifiers(qualifiers), - } - } - } else if self.is_own_dataclass_instance_field(db, name) - && declared_ty - .class_member(db, "__get__".into()) - .place - .is_undefined() - { - // For dataclass-like classes, declared fields are assigned - // by the synthesized `__init__`, so they are instance - // attributes even without an explicit `self.x = ...` - // assignment in a method body. - // - // However, if the declared type is a descriptor (has - // `__get__`), we return unbound so that the descriptor - // protocol in `member_lookup_with_policy` can resolve - // the attribute type through `__get__`. - Member { - inner: declared.with_qualifiers(qualifiers), - } - } else { - // The symbol is declared and bound in the class body, - // but we did not find any attribute assignments in - // methods of the class. This means that the attribute - // has a class-level default value, but it would not be - // found in a `__dict__` lookup. - - Member::unbound() - } - } else { - // The attribute is declared but not bound in the class body. - // We take this as a sign that this is intended to be a pure - // instance attribute, and we trust the declared type, unless - // it is possibly-undeclared. In the latter case, we also - // union with the inferred type from attribute assignments. - - if declaredness == Definedness::AlwaysDefined { - Member { - inner: declared.with_qualifiers(qualifiers), - } - } else { - if let Some(implicit_ty) = Self::implicit_attribute( - db, - body_scope, - name, - MethodDecorator::None, - ) - .inner - .place - .ignore_possibly_undefined() - { - Member { - inner: Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements( - db, - declared_ty, - implicit_ty, - ), - origin: TypeOrigin::Declared, - definedness: declaredness, - widening: Widening::None, - }) - .with_qualifiers(qualifiers), - } - } else { - Member { - inner: declared.with_qualifiers(qualifiers), - } - } - } - } - } - - PlaceAndQualifiers { - place: Place::Undefined, - qualifiers: _, - } => { - // The attribute is not *declared* in the class body. It could still be declared/bound - // in a method. - - Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) - } - } - } else { - // This attribute is neither declared nor bound in the class body. - // It could still be implicitly defined in a method. - - Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) - } - } - - /// Returns `true` if `name` is a non-init-only field directly declared on this - /// dataclass (i.e., a field that corresponds to an instance attribute). - /// - /// This is used to decide whether a bare class-body annotation like `x: int` - /// should be treated as defining an instance attribute: dataclass fields are - /// implicitly assigned in `__init__`, so they behave as instance attributes - /// even though no explicit binding exists in the class body. - fn is_own_dataclass_instance_field(self, db: &'db dyn Db, name: &str) -> bool { - let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { - return false; - }; - if !matches!(field_policy, CodeGeneratorKind::DataclassLike(_)) { - return false; - } - - let fields = self.own_fields(db, None, field_policy); - let Some(field) = fields.get(name) else { - return false; - }; - matches!( - field.kind, - FieldKind::Dataclass { - init_only: false, - .. - } - ) - } - - pub(super) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { - Type::instance(db, ClassType::NonGeneric(self.into())) - } - - /// Return this class' involvement in an inheritance cycle, if any. - /// - /// A class definition like this will fail at runtime, - /// but we must be resilient to it or we could panic. - #[salsa::tracked(cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { - /// Return `true` if the class is cyclically defined. - /// - /// Also, populates `visited_classes` with all base classes of `self`. - fn is_cyclically_defined_recursive<'db>( - db: &'db dyn Db, - class: StaticClassLiteral<'db>, - classes_on_stack: &mut IndexSet>, - visited_classes: &mut IndexSet>, - ) -> bool { - let mut result = false; - for explicit_base in class.explicit_bases(db) { - let explicit_base_class_literal = match explicit_base { - Type::ClassLiteral(class_literal) => class_literal.as_static(), - Type::GenericAlias(generic_alias) => Some(generic_alias.origin(db)), - _ => continue, - }; - let Some(explicit_base_class_literal) = explicit_base_class_literal else { - continue; - }; - if !classes_on_stack.insert(explicit_base_class_literal) { - return true; - } - - if visited_classes.insert(explicit_base_class_literal) { - // If we find a cycle, keep searching to check if we can reach the starting class. - result |= is_cyclically_defined_recursive( - db, - explicit_base_class_literal, - classes_on_stack, - visited_classes, - ); - } - classes_on_stack.pop(); - } - result - } - - tracing::trace!("Class::inheritance_cycle: {}", self.name(db)); - - let visited_classes = &mut IndexSet::new(); - if !is_cyclically_defined_recursive(db, self, &mut IndexSet::new(), visited_classes) { - None - } else if visited_classes.contains(&self) { - Some(InheritanceCycle::Participant) - } else { - Some(InheritanceCycle::Inherited) - } - } - - /// Returns a [`Span`] with the range of the class's header. - /// - /// See [`Self::header_range`] for more details. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { - Span::from(self.file(db)).with_range(self.header_range(db)) - } - - /// Returns the range of the class's "header": the class name - /// and any arguments passed to the `class` statement. E.g. - /// - /// ```ignore - /// class Foo(Bar, metaclass=Baz): ... - /// ^^^^^^^^^^^^^^^^^^^^^^^ - /// ``` - pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { - let class_scope = self.body_scope(db); - let module = parsed_module(db, class_scope.file(db)).load(db); - let class_node = class_scope.node(db).expect_class().node(&module); - let class_name = &class_node.name; - TextRange::new( - class_name.start(), - class_node - .arguments - .as_deref() - .map(Ranged::end) - .unwrap_or_else(|| class_name.end()), - ) - } -} - -#[salsa::tracked] -impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { - #[salsa::tracked(cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { - let typevar_in_generic_context = self - .generic_context(db) - .is_some_and(|generic_context| generic_context.variables(db).contains(&typevar)); - - if !typevar_in_generic_context { - return TypeVarVariance::Bivariant; - } - let class_body_scope = self.body_scope(db); - - let file = class_body_scope.file(db); - let index = semantic_index(db, file); - - let explicit_bases_variances = self - .explicit_bases(db) - .iter() - .map(|class| class.variance_of(db, typevar)); - - let default_attribute_variance = { - let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self.into(), None); - // Python 3.13 introduced a synthesized `__replace__` method on dataclasses which uses - // their field types in contravariant position, thus meaning a frozen dataclass must - // still be invariant in its field types. Other synthesized methods on dataclasses are - // not considered here, since they don't use field types in their signatures. TODO: - // ideally we'd have a single source of truth for information about synthesized - // methods, so we just look them up normally and don't hardcode this knowledge here. - let is_frozen_dataclass = Program::get(db).python_version(db) <= PythonVersion::PY312 - && self - .dataclass_params(db) - .is_some_and(|params| params.flags(db).contains(DataclassFlags::FROZEN)); - if is_namedtuple || is_frozen_dataclass { - TypeVarVariance::Covariant - } else { - TypeVarVariance::Invariant - } - }; - - let init_name: &Name = &"__init__".into(); - let new_name: &Name = &"__new__".into(); - - let use_def_map = index.use_def_map(class_body_scope.file_scope_id(db)); - let table = place_table(db, class_body_scope); - let attribute_places_and_qualifiers = - use_def_map - .all_end_of_scope_symbol_declarations() - .map(|(symbol_id, declarations)| { - let place_and_qual = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); - (symbol_id, place_and_qual) - }) - .chain(use_def_map.all_end_of_scope_symbol_bindings().map( - |(symbol_id, bindings)| { - (symbol_id, place_from_bindings(db, bindings).place.into()) - }, - )) - .filter_map(|(symbol_id, place_and_qual)| { - if let Some(name) = table.place(symbol_id).as_symbol().map(Symbol::name) { - (![init_name, new_name].contains(&name)) - .then_some((name.to_string(), place_and_qual)) - } else { - None - } - }); - - // Dataclasses can have some additional synthesized methods (`__eq__`, `__hash__`, - // `__lt__`, etc.) but none of these will have field types type variables in their signatures, so we - // don't need to consider them for variance. - - let attribute_names = attribute_scopes(db, self.body_scope(db)) - .flat_map(|function_scope_id| { - index - .place_table(function_scope_id) - .members() - .filter_map(|member| member.as_instance_attribute()) - .filter(|name| *name != init_name && *name != new_name) - .map(std::string::ToString::to_string) - .collect::>() - }) - .dedup(); - - let attribute_variances = attribute_names - .map(|name| { - let place_and_quals = self.own_instance_member(db, &name).inner; - (name, place_and_quals) - }) - .chain(attribute_places_and_qualifiers) - .dedup() - .filter_map(|(name, place_and_qual)| { - place_and_qual.ignore_possibly_undefined().map(|ty| { - let variance = if place_and_qual - .qualifiers - // `CLASS_VAR || FINAL` is really `all()`, but - // we want to be robust against new qualifiers - .intersects(TypeQualifiers::CLASS_VAR | TypeQualifiers::FINAL) - // We don't allow mutation of methods or properties - || ty.is_function_literal() - || ty.is_property_instance() - // Underscore-prefixed attributes are assumed not to be externally mutated - || name.starts_with('_') - { - // CLASS_VAR: class vars generally shouldn't contain the - // type variable, but they could if it's a - // callable type. They can't be mutated on instances. - // - // FINAL: final attributes are immutable, and thus covariant - TypeVarVariance::Covariant - } else { - default_attribute_variance - }; - ty.with_polarity(variance).variance_of(db, typevar) - }) - }); - - attribute_variances - .chain(explicit_bases_variances) - .collect() - } -} - -impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { - match self { - Self::Static(class) => class.variance_of(db, typevar), - Self::Dynamic(_) | Self::DynamicNamedTuple(_) => TypeVarVariance::Bivariant, - } - } -} - -/// A class created dynamically via a three-argument `type()` call. -/// -/// For example: -/// ```python -/// Foo = type("Foo", (Base,), {"attr": 1}) -/// ``` -/// -/// The type of `Foo` would be `` where `Foo` is a `DynamicClassLiteral` with: -/// - name: "Foo" -/// - members: [("attr", int)] -/// -/// This is called "dynamic" because the class is created dynamically at runtime -/// via a function call rather than a class statement. -/// -/// # Salsa interning -/// -/// This is a Salsa-interned struct. Two different `type()` calls always produce -/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases: -/// -/// ```python -/// Foo1 = type("Foo", (Base,), {}) -/// Foo2 = type("Foo", (Base,), {}) -/// # Foo1 and Foo2 are distinct types -/// ``` -/// -/// The `anchor` field provides stable identity: -/// - For assigned `type()` calls, the `Definition` uniquely identifies the class. -/// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope -/// provides stable identity that only changes when the scope itself changes. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct DynamicClassLiteral<'db> { - /// The name of the class (from the first argument to `type()`). - #[returns(ref)] - pub name: Name, - - /// The anchor for this dynamic class, providing stable identity. - /// - /// - `Definition`: The `type()` call is assigned to a variable. The definition - /// uniquely identifies this class and can be used to find the `type()` call. - /// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset - /// is relative to the enclosing scope's anchor node index. - #[returns(ref)] - pub anchor: DynamicClassAnchor<'db>, - - /// The class members from the namespace dict (third argument to `type()`). - /// Each entry is a (name, type) pair extracted from the dict literal. - #[returns(deref)] - pub members: Box<[(Name, Type<'db>)]>, - - /// Whether the namespace dict (third argument) is dynamic (not a literal dict, - /// or contains non-string-literal keys). When true, attribute lookups on this - /// class and its instances return `Unknown` instead of failing. - pub has_dynamic_namespace: bool, - - /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator - /// or passed to `dataclass()` as a function. - pub dataclass_params: Option>, -} - -/// Anchor for identifying a dynamic class literal. -/// -/// This enum provides stable identity for `DynamicClassLiteral`: -/// - For assigned calls, the `Definition` uniquely identifies the class. -/// - For dangling calls, a relative offset provides stable identity. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub enum DynamicClassAnchor<'db> { - /// The `type()` call is assigned to a variable. - /// - /// The `Definition` uniquely identifies this class. The `type()` call expression - /// is the `value` of the assignment, so we can get its range from the definition. - Definition(Definition<'db>), - - /// The `type()` call is "dangling" (not assigned to a variable). - /// - /// The offset is relative to the enclosing scope's anchor node index. - /// For module scope, this is equivalent to an absolute index (anchor is 0). - /// - /// The `explicit_bases` are computed eagerly at creation time since dangling - /// calls cannot recursively reference the class being defined. - ScopeOffset { - scope: ScopeId<'db>, - offset: u32, - explicit_bases: Box<[Type<'db>]>, - }, -} - -impl get_size2::GetSize for DynamicClassLiteral<'_> {} - -#[salsa::tracked] -impl<'db> DynamicClassLiteral<'db> { - /// Returns the definition where this class is created, if it was assigned to a variable. - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { - match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => Some(*definition), - DynamicClassAnchor::ScopeOffset { .. } => None, - } - } - - /// Returns the scope in which this dynamic class was created. - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => definition.scope(db), - DynamicClassAnchor::ScopeOffset { scope, .. } => *scope, - } - } - - /// Returns the explicit base classes of this dynamic class. - /// - /// For assigned `type()` calls, bases are computed lazily using deferred inference - /// to handle forward references (e.g., `X = type("X", (tuple["X | None"],), {})`). - /// - /// For dangling `type()` calls, bases are computed eagerly at creation time and - /// stored directly on the anchor, since dangling calls cannot recursively reference - /// the class being defined. - /// - /// Returns an empty slice if the bases cannot be computed (e.g., due to a cycle) - /// or if the bases argument is not a tuple. - /// - /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). - pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { - /// Inner cached function for deferred inference of bases. - /// Only called for assigned `type()` calls where inference was deferred. - #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] - fn deferred_explicit_bases<'db>( - db: &'db dyn Db, - definition: Definition<'db>, - ) -> Box<[Type<'db>]> { - let module = parsed_module(db, definition.file(db)).load(db); - - let value = definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments"); - let call_expr = value - .as_call_expr() - .expect("Definition value should be a call expression"); - - // The `bases` argument is the second positional argument. - let Some(bases_arg) = call_expr.arguments.args.get(1) else { - return Box::default(); - }; - - // Use `definition_expression_type` for deferred inference support. - let bases_type = definition_expression_type(db, definition, bases_arg); - - // For variable-length tuples (like `tuple[type, ...]`), we can't statically - // determine the bases, so return Unknown. - bases_type - .fixed_tuple_elements(db) - .map(Cow::into_owned) - .map(Into::into) - .unwrap_or_else(|| Box::from([Type::unknown()])) - } - - match self.anchor(db) { - // For dangling calls, bases are stored directly on the anchor. - DynamicClassAnchor::ScopeOffset { explicit_bases, .. } => explicit_bases.as_ref(), - // For assigned calls, use deferred inference. - DynamicClassAnchor::Definition(definition) => deferred_explicit_bases(db, *definition), - } - } - - /// Returns a [`Span`] with the range of the `type()` call expression. - /// - /// See [`Self::header_range`] for more details. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { - Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) - } - - /// Returns the range of the `type()` call expression that created this class. - pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => { - // For definitions, get the range from the definition's value. - // The `type()` call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") - .range() - } - DynamicClassAnchor::ScopeOffset { offset, .. } => { - // For dangling `type()` calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + *offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() - } - } - } - - /// Get the metaclass of this dynamic class. - /// - /// Derives the metaclass from base classes: finds the most derived metaclass - /// that is a subclass of all other base metaclasses. - /// - /// See - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - self.try_metaclass(db) - .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) - } - - /// Try to get the metaclass of this dynamic class. - /// - /// Returns `Err(DynamicMetaclassConflict)` if there's a metaclass conflict - /// (i.e., two base classes have metaclasses that are not in a subclass relationship). - /// - /// See - pub(crate) fn try_metaclass( - self, - db: &'db dyn Db, - ) -> Result, DynamicMetaclassConflict<'db>> { - let original_bases = self.explicit_bases(db); - - // If no bases, metaclass is `type`. - // To dynamically create a class with no bases that has a custom metaclass, - // you have to invoke that metaclass rather than `type()`. - if original_bases.is_empty() { - return Ok(KnownClass::Type.to_class_literal(db)); - } - - // If there's an MRO error, return unknown to avoid cascading errors. - if self.try_mro(db).is_err() { - return Ok(SubclassOfType::subclass_of_unknown()); - } - - // Convert Types to ClassBases for metaclass computation. - // All bases should convert successfully here: `try_mro()` above would have - // returned `Err(InvalidBases)` if any failed, causing us to return early. - let bases: Vec> = original_bases - .iter() - .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, None)) - .collect(); - - // If all bases failed to convert, return type as the metaclass. - if bases.is_empty() { - return Ok(KnownClass::Type.to_class_literal(db)); - } - - // Start with the first base's metaclass as the candidate. - let mut candidate = bases[0].metaclass(db); - - // Track which base the candidate metaclass came from. - let (mut candidate_base, rest) = bases.split_first().unwrap(); - - // Reconcile with other bases' metaclasses. - for base in rest { - let base_metaclass = base.metaclass(db); - - // Get the ClassType for comparison. - let Some(candidate_class) = candidate.to_class_type(db) else { - // If candidate isn't a class type, keep it as is. - continue; - }; - let Some(base_metaclass_class) = base_metaclass.to_class_type(db) else { - continue; - }; - - // If base's metaclass is more derived, use it. - if base_metaclass_class.is_subclass_of(db, candidate_class) { - candidate = base_metaclass; - candidate_base = base; - continue; - } - - // If candidate is already more derived, keep it. - if candidate_class.is_subclass_of(db, base_metaclass_class) { - continue; - } - - // Conflict: neither metaclass is a subclass of the other. - // Python raises `TypeError: metaclass conflict` at runtime. - return Err(DynamicMetaclassConflict { - metaclass1: candidate_class, - base1: *candidate_base, - metaclass2: base_metaclass_class, - base2: *base, - }); - } - - Ok(candidate) - } - - /// Iterate over the MRO of this class using C3 linearization. - /// - /// The MRO includes the class itself as the first element, followed - /// by the merged base class MROs (consistent with `ClassType::iter_mro`). - /// - /// If the MRO cannot be computed (e.g., due to inconsistent ordering), falls back - /// to iterating over base MROs sequentially with deduplication. - pub(crate) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { - MroIterator::new(db, ClassLiteral::Dynamic(self), None) - } - - /// Look up an instance member by iterating through the MRO. - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - match MroLookup::new(db, self.iter_mro(db)).instance_member(name) { - InstanceMemberResult::Done(result) => result, - InstanceMemberResult::TypedDict => { - // Simplified `TypedDict` handling without type mapping. - KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) - } - } - } - - /// Look up a class-level member by iterating through the MRO. - /// - /// Uses `MroLookup` with: - /// - No inherited generic context (dynamic classes aren't generic). - /// - `is_self_object = false` (dynamic classes are never `object`). - pub(crate) fn class_member( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - // Check if this dynamic class is dataclass-like (via dataclass_transform inheritance). - if matches!( - CodeGeneratorKind::from_class(db, self.into(), None), - Some(CodeGeneratorKind::DataclassLike(_)) - ) { - if name == "__dataclass_fields__" { - // Make this class look like a subclass of the `DataClassInstance` protocol. - return Place::declared(KnownClass::Dict.to_specialized_instance( - db, - &[ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, &[Type::any()]), - ], - )) - .with_qualifiers(TypeQualifiers::CLASS_VAR); - } else if name == "__dataclass_params__" { - // There is no typeshed class for this. For now, we model it as `Any`. - return Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR); - } - } - - let result = MroLookup::new(db, self.iter_mro(db)).class_member( - name, policy, None, // No inherited generic context. - false, // Dynamic classes are never `object`. - ); - - match result { - ClassMemberResult::Done(result) => result.finalize(db), - ClassMemberResult::TypedDict => { - // Simplified `TypedDict` handling without type mapping. - KnownClass::TypedDictFallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) - .expect("Will return Some() when called on class literal") - } - } - } - - /// Look up a class member defined directly on this class (not inherited). - /// - /// Returns [`Member::unbound`] if the member is not found in the namespace dict, - /// unless the namespace is dynamic, in which case returns `Unknown`. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - // If the namespace is dynamic (not a literal dict) and the name isn't in `self.members`, - // return Unknown since we can't know what attributes might be defined. - self.members(db) - .iter() - .find_map(|(member_name, ty)| (name == member_name).then_some(*ty)) - .or_else(|| self.has_dynamic_namespace(db).then(Type::unknown)) - .map(Member::definitely_declared) - .unwrap_or_default() - } - - /// Look up an instance member defined directly on this class (not inherited). - /// - /// For dynamic classes, instance members are the same as class members - /// since they come from the namespace dict. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - self.own_class_member(db, name) - } - - /// Try to compute the MRO for this dynamic class. - /// - /// Returns `Ok(Mro)` if successful, or `Err(DynamicMroError)` if there's - /// an error (duplicate bases or C3 linearization failure). - #[salsa::tracked(returns(ref), cycle_initial=dynamic_class_try_mro_cycle_initial, heap_size = ruff_memory_usage::heap_size)] - pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> { - Mro::of_dynamic_class(db, self) - } - - /// Return `Some()` if this dynamic class is known to be a [`DisjointBase`]. - /// - /// A dynamic class is a disjoint base if `__slots__` is defined in the namespace - /// dictionary and is non-empty. Example: - /// ```python - /// X = type("X", (), {"__slots__": ("a",)}) - /// ``` - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { - // Check if __slots__ is in the members - for (name, ty) in self.members(db) { - if name.as_str() == "__slots__" { - // Check if the slots are non-empty - let is_non_empty = match ty { - // __slots__ = ("a", "b") - Type::NominalInstance(nominal) => nominal.tuple_spec(db).is_some_and(|spec| { - spec.len().into_fixed_length().is_some_and(|len| len > 0) - }), - // __slots__ = "abc" # Same as ("abc",) - Type::LiteralValue(literal) if literal.is_string() => true, - // Other types are considered dynamic/unknown - _ => false, - }; - if is_non_empty { - return Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Dynamic( - self, - ))); - } - } - } - None - } - - /// Returns `true` if this dynamic class defines any ordering method (`__lt__`, `__le__`, - /// `__gt__`, `__ge__`) in its namespace dictionary. Used by `@total_ordering` to determine - /// if synthesis is valid. - /// - /// If the namespace is dynamic, returns `true` since we can't know if ordering methods exist. - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { - const ORDERING_METHODS: &[&str] = &["__lt__", "__le__", "__gt__", "__ge__"]; - ORDERING_METHODS - .iter() - .any(|name| !self.own_class_member(db, name).is_undefined()) - } - - /// Returns a new [`DynamicClassLiteral`] with the given dataclass params, preserving all other fields. - pub(crate) fn with_dataclass_params( - self, - db: &'db dyn Db, - dataclass_params: Option>, - ) -> Self { - Self::new( - db, - self.name(db).clone(), - self.anchor(db).clone(), - self.members(db), - self.has_dynamic_namespace(db), - dataclass_params, - ) - } -} - -/// Error for metaclass conflicts in dynamic classes. -/// -/// This mirrors `MetaclassErrorKind::Conflict` for regular classes. -#[derive(Debug, Clone)] -pub(crate) struct DynamicMetaclassConflict<'db> { - /// The first conflicting metaclass and its originating base class. - pub(crate) metaclass1: ClassType<'db>, - pub(crate) base1: ClassBase<'db>, - /// The second conflicting metaclass and its originating base class. - pub(crate) metaclass2: ClassType<'db>, - pub(crate) base2: ClassBase<'db>, -} - -/// Create a property type for a namedtuple field. -fn create_field_property<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Type<'db> { - let property_getter_signature = Signature::new( - Parameters::new( - db, - [Parameter::positional_only(Some(Name::new_static("self")))], - ), - field_ty, - ); - let property_getter = Type::single_callable(db, property_getter_signature); - let property = PropertyInstanceType::new(db, Some(property_getter), None); - Type::PropertyInstance(property) -} - -/// Synthesize a namedtuple class member given the field information. -/// -/// This is used by both `DynamicNamedTupleLiteral` and `StaticClassLiteral` (for declarative -/// namedtuples) to avoid duplicating the synthesis logic. -/// -/// The `inherited_generic_context` parameter is used for declarative namedtuples to preserve -/// generic context in the synthesized `__new__` signature. -fn synthesize_namedtuple_class_member<'db>( - db: &'db dyn Db, - name: &str, - instance_ty: Type<'db>, - fields: impl Iterator>, - inherited_generic_context: Option>, -) -> Option> { - match name { - "__new__" => { - // __new__(cls, field1, field2, ...) -> Self - let self_typevar = - BoundTypeVarInstance::synthetic_self(db, instance_ty, BindingContext::Synthetic); - let self_ty = Type::TypeVar(self_typevar); - - let variables = inherited_generic_context - .iter() - .flat_map(|ctx| ctx.variables(db)) - .chain(std::iter::once(self_typevar)); - - let generic_context = GenericContext::from_typevar_instances(db, variables); - - let first_parameter = Parameter::positional_or_keyword(Name::new_static("cls")) - .with_annotated_type(SubclassOfType::from(db, self_typevar)); - - let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { - Parameter::positional_or_keyword(field.name) - .with_annotated_type(field.ty) - .with_optional_default_type(field.default) - })); - - let signature = Signature::new_generic( - Some(generic_context), - Parameters::new(db, parameters), - self_ty, - ); - Some(Type::function_like_callable(db, signature)) - } - "_fields" => { - // _fields: tuple[Literal["field1"], Literal["field2"], ...] - let field_types = fields.map(|field| Type::string_literal(db, &field.name)); - Some(Type::heterogeneous_tuple(db, field_types)) - } - "__slots__" => { - // __slots__: tuple[()] - always empty for namedtuples - Some(Type::empty_tuple(db)) - } - "_replace" | "__replace__" => { - if name == "__replace__" && Program::get(db).python_version(db) < PythonVersion::PY313 { - return None; - } - - // _replace(self, *, field1=..., field2=...) -> Self - let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self( - db, - instance_ty, - BindingContext::Synthetic, - )); - - let first_parameter = Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(self_ty); - - let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { - Parameter::keyword_only(field.name) - .with_annotated_type(field.ty) - .with_default_type(field.ty) - })); - - let signature = Signature::new(Parameters::new(db, parameters), self_ty); - Some(Type::function_like_callable(db, signature)) - } - "__init__" => { - // Namedtuples don't have a custom __init__. All construction happens in __new__. - None - } - _ => { - // Fall back to NamedTupleFallback for other synthesized methods. - KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .as_static()? - .own_class_member(db, inherited_generic_context, None, name) - .ignore_possibly_undefined() - } - } -} - -#[derive(Debug, salsa::Update, get_size2::GetSize, Clone, PartialEq, Eq, Hash)] -pub struct NamedTupleField<'db> { - pub(crate) name: Name, - pub(crate) ty: Type<'db>, - pub(crate) default: Option>, -} - -/// A namedtuple created via the functional form `namedtuple(name, fields)` or -/// `NamedTuple(name, fields)`. -/// -/// For example: -/// ```python -/// from collections import namedtuple -/// Point = namedtuple("Point", ["x", "y"]) -/// -/// from typing import NamedTuple -/// Person = NamedTuple("Person", [("name", str), ("age", int)]) -/// ``` -/// -/// The type of `Point` would be `type[Point]` where `Point` is a `DynamicNamedTupleLiteral`. -#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] -pub struct DynamicNamedTupleLiteral<'db> { - /// The name of the namedtuple (from the first argument). - #[returns(ref)] - pub name: Name, - - /// The anchor for this dynamic namedtuple, providing stable identity. - /// - /// - `Definition`: The call is assigned to a variable. The definition - /// uniquely identifies this namedtuple and can be used to find the call. - /// - `ScopeOffset`: The call is "dangling" (not assigned). The offset - /// is relative to the enclosing scope's anchor node index. - #[returns(ref)] - pub anchor: DynamicNamedTupleAnchor<'db>, -} - -impl get_size2::GetSize for DynamicNamedTupleLiteral<'_> {} - -#[salsa::tracked] -impl<'db> DynamicNamedTupleLiteral<'db> { - /// Returns the definition where this namedtuple is created, if it was assigned to a variable. - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { - match self.anchor(db) { - DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } - | DynamicNamedTupleAnchor::TypingDefinition(definition) => Some(*definition), - DynamicNamedTupleAnchor::ScopeOffset { .. } => None, - } - } - - /// Returns the scope in which this dynamic class was created. - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - match self.anchor(db) { - DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } - | DynamicNamedTupleAnchor::TypingDefinition(definition) => definition.scope(db), - DynamicNamedTupleAnchor::ScopeOffset { scope, .. } => *scope, - } - } - - /// Returns an instance type for this dynamic namedtuple. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { - Type::instance(db, ClassType::NonGeneric(self.into())) - } - - /// Returns the range of the namedtuple call expression. - pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { - DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } - | DynamicNamedTupleAnchor::TypingDefinition(definition) => { - // For definitions, get the range from the definition's value. - // The namedtuple call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") - .range() - } - DynamicNamedTupleAnchor::ScopeOffset { offset, .. } => { - // For dangling calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() - } - } - } - - /// Returns a [`Span`] pointing to the namedtuple call expression. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { - Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) - } - - /// Compute the MRO for this namedtuple. - /// - /// The MRO is the MRO of the class's tuple base class, prepended by `self`. - /// For example, `namedtuple("Point", [("x", int), ("y", int)])` has the following MRO: - /// - /// 1. `` - /// 2. `` - /// 3. `` - /// 4. `` - /// 5. `` - /// 6. `` - /// 7. `` - /// 8. `typing.Protocol` - /// 9. `typing.Generic` - /// 10. `` - #[salsa::tracked( - returns(ref), - heap_size=ruff_memory_usage::heap_size, - cycle_initial=dynamic_namedtuple_mro_cycle_initial - )] - pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { - let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); - let tuple_class = self.tuple_base_class(db); - std::iter::once(self_base) - .chain(tuple_class.iter_mro(db)) - .collect() - } - - /// Get the metaclass of this dynamic namedtuple. - /// - /// Namedtuples always have `type` as their metaclass. - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - let _ = self; - KnownClass::Type.to_class_literal(db) - } - - /// Compute the specialized tuple class that this namedtuple inherits from. - /// - /// For example, `namedtuple("Point", [("x", int), ("y", int)])` inherits from `tuple[int, int]`. - pub(crate) fn tuple_base_class(self, db: &'db dyn Db) -> ClassType<'db> { - // If fields are unknown, return `tuple[Unknown, ...]` to avoid false positives - // like index-out-of-bounds errors. - if !self.has_known_fields(db) { - return TupleType::homogeneous(db, Type::unknown()).to_class_type(db); - } - - let field_types = self.fields(db).iter().map(|field| field.ty); - TupleType::heterogeneous(db, field_types) - .map(|t| t.to_class_type(db)) - .unwrap_or_else(|| { - KnownClass::Tuple - .to_class_literal(db) - .as_class_literal() - .expect("tuple should be a class literal") - .default_specialization(db) - }) - } - - /// Look up an instance member defined directly on this class (not inherited). - /// - /// For dynamic namedtuples, instance members are the field names. - /// If fields are unknown (dynamic), returns `Any` for any attribute. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - for field in self.fields(db) { - if field.name == name { - return Member::definitely_declared(field.ty); - } - } - - if !self.has_known_fields(db) { - return Member::definitely_declared(Type::any()); - } - - Member::unbound() - } - - /// Look up an instance member by name (including superclasses). - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - // First check own instance members. - let result = self.own_instance_member(db, name); - if !result.is_undefined() { - return result.inner; - } - - // Fall back to the tuple base type for other attributes. - Type::instance(db, self.tuple_base_class(db)).instance_member(db, name) - } - - /// Look up a class-level member by name. - pub(crate) fn class_member( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { - // First check synthesized members and fields. - let member = self.own_class_member(db, name); - if !member.is_undefined() { - return member.inner; - } - - // Fall back to tuple class members. - let result = self - .tuple_base_class(db) - .class_literal(db) - .class_member(db, name, policy); - - // If fields are unknown (dynamic) and the attribute wasn't found, - // return `Any` instead of failing. - if !self.has_known_fields(db) && result.place.is_undefined() { - return Place::bound(Type::any()).into(); - } - - result - } - - /// Look up a class-level member defined directly on this class (not inherited). - /// - /// This only checks synthesized members and field properties, without falling - /// back to tuple or other base classes. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { - // Handle synthesized namedtuple attributes. - if let Some(ty) = self.synthesized_class_member(db, name) { - return Member::definitely_declared(ty); - } - - // Check if it's a field name (returns a property descriptor). - for field in self.fields(db) { - if field.name == name { - return Member::definitely_declared(create_field_property(db, field.ty)); - } - } - - Member::default() - } - - /// Generate synthesized class members for namedtuples. - fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option> { - let instance_ty = self.to_instance(db); - - // When fields are unknown, handle constructor and field-specific methods specially. - if !self.has_known_fields(db) { - match name { - // For constructors, return a gradual signature that accepts any arguments. - "__new__" | "__init__" => { - let signature = Signature::new(Parameters::gradual_form(), instance_ty); - return Some(Type::function_like_callable(db, signature)); - } - // For other field-specific methods, fall through to NamedTupleFallback. - "_fields" | "_replace" | "__replace__" => { - return KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .as_static()? - .own_class_member(db, None, None, name) - .ignore_possibly_undefined() - .map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: instance_ty, - }, - TypeContext::default(), - ) - }); - } - _ => {} - } - } - - let result = synthesize_namedtuple_class_member( - db, - name, - instance_ty, - self.fields(db).iter().cloned(), - None, - ); - // For fallback members from NamedTupleFallback, apply type mapping to handle - // `Self` types. The explicitly synthesized members (__new__, _fields, _replace, - // __replace__) don't need this mapping. - if matches!( - name, - "__new__" | "_fields" | "_replace" | "__replace__" | "__slots__" - ) { - result - } else { - result.map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: instance_ty, - }, - TypeContext::default(), - ) - }) - } - } - - fn spec(self, db: &'db dyn Db) -> NamedTupleSpec<'db> { - #[salsa::tracked(cycle_initial=deferred_spec_initial, heap_size=ruff_memory_usage::heap_size)] - fn deferred_spec<'db>(db: &'db dyn Db, definition: Definition<'db>) -> NamedTupleSpec<'db> { - let module = parsed_module(db, definition.file(db)).load(db); - let node = definition - .kind(db) - .value(&module) - .expect("Expected `NamedTuple` definition to be an assignment") - .as_call_expr() - .expect("Expected `NamedTuple` definition r.h.s. to be a call expression"); - match definition_expression_type(db, definition, &node.arguments.args[1]) { - Type::KnownInstance(KnownInstanceType::NamedTupleSpec(spec)) => spec, - _ => NamedTupleSpec::unknown(db), - } - } - - fn deferred_spec_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - _definition: Definition<'db>, - ) -> NamedTupleSpec<'db> { - NamedTupleSpec::unknown(db) - } - - match self.anchor(db) { - DynamicNamedTupleAnchor::CollectionsDefinition { spec, .. } - | DynamicNamedTupleAnchor::ScopeOffset { spec, .. } => *spec, - DynamicNamedTupleAnchor::TypingDefinition(definition) => deferred_spec(db, *definition), - } - } - - fn fields(self, db: &'db dyn Db) -> &'db [NamedTupleField<'db>] { - self.spec(db).fields(db) - } - - fn has_known_fields(self, db: &'db dyn Db) -> bool { - self.spec(db).has_known_fields(db) - } -} - -fn dynamic_namedtuple_mro_cycle_initial<'db>( - db: &'db dyn Db, - _id: salsa::Id, - self_: DynamicNamedTupleLiteral<'db>, -) -> Mro<'db> { - Mro::from_error( - db, - ClassType::NonGeneric(ClassLiteral::DynamicNamedTuple(self_)), - ) -} - -/// Anchor for identifying a dynamic `namedtuple`/`NamedTuple` class literal. -/// -/// This enum provides stable identity for `DynamicNamedTupleLiteral` instances: -/// - For assigned calls, the `Definition` uniquely identifies the class. -/// - For dangling calls, a relative offset provides stable identity. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub enum DynamicNamedTupleAnchor<'db> { - /// We're dealing with a `collections.namedtuple()` call - /// that's assigned to a variable. - /// - /// The `Definition` uniquely identifies this class. The `namedtuple()` - /// call expression is the `value` of the assignment, so we can get its - /// range from the definition. - CollectionsDefinition { - definition: Definition<'db>, - spec: NamedTupleSpec<'db>, - }, - - /// We're dealing with a `typing.NamedTuple()` call - /// that's assigned to a variable. - /// - /// The `Definition` uniquely identifies this class. The `NamedTuple()` - /// call expression is the `value` of the assignment, so we can get its - /// range from the definition. - /// - /// Unlike the `CollectionsDefinition` variant, this variant does not - /// hold a `NamedTupleSpec`. This is because the spec for a - /// `typing.NamedTuple` call can contain forward references and recursive - /// references that must be evaluated lazily. The spec is computed - /// on-demand from the definition. - TypingDefinition(Definition<'db>), - - /// We're dealing with a `namedtuple()` or `NamedTuple` call that is - /// "dangling" (not assigned to a variable). - /// - /// The offset is relative to the enclosing scope's anchor node index. - /// For module scope, this is equivalent to an absolute index (anchor is 0). - /// - /// Dangling calls can always store the spec. They *can* contain - /// forward references if they appear in class bases: - /// - /// ```python - /// from typing import NamedTuple - /// - /// class F(NamedTuple("F", [("x", "F | None")]): - /// pass - /// ``` - /// - /// But this doesn't matter, because all class bases are deferred in their - /// entirety during type inference. - ScopeOffset { - scope: ScopeId<'db>, - offset: u32, - spec: NamedTupleSpec<'db>, - }, -} - -/// A specification describing the fields of a dynamic `namedtuple` -/// or `NamedTuple` class. -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct NamedTupleSpec<'db> { - #[returns(deref)] - pub(crate) fields: Box<[NamedTupleField<'db>]>, - - pub(crate) has_known_fields: bool, -} - -impl<'db> NamedTupleSpec<'db> { - /// Create a [`NamedTupleSpec`] with the given fields. - pub(crate) fn known(db: &'db dyn Db, fields: Box<[NamedTupleField<'db>]>) -> Self { - Self::new(db, fields, true) - } - - /// Create a [`NamedTupleSpec`] that indicates a namedtuple class has unknown fields. - pub(crate) fn unknown(db: &'db dyn Db) -> Self { - Self::new(db, Box::default(), false) - } - - pub(crate) fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - let fields = self - .fields(db) - .iter() - .map(|f| { - Some(NamedTupleField { - name: f.name.clone(), - ty: if nested { - f.ty.recursive_type_normalized_impl(db, div, nested)? - } else { - f.ty.recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div) - }, - default: None, - }) - }) - .collect::>>()?; - - Some(Self::new(db, fields, self.has_known_fields(db))) - } -} - -impl get_size2::GetSize for NamedTupleSpec<'_> {} - -/// Performs member lookups over an MRO (Method Resolution Order). -/// -/// This struct encapsulates the shared logic for looking up class and instance -/// members by iterating through an MRO. Both `StaticClassLiteral` and `DynamicClassLiteral` -/// use this to avoid duplicating the MRO traversal logic. -pub(super) struct MroLookup<'db, I> { - db: &'db dyn Db, - mro_iter: I, -} - -impl<'db, I: Iterator>> MroLookup<'db, I> { - /// Create a new MRO lookup from a database and an MRO iterator. - pub(super) fn new(db: &'db dyn Db, mro_iter: I) -> Self { - Self { db, mro_iter } - } - - /// Look up a class member by iterating through the MRO. - /// - /// Parameters: - /// - `name`: The member name to look up - /// - `policy`: Controls which classes in the MRO to skip - /// - `inherited_generic_context`: Generic context for `own_class_member` calls - /// - `is_self_object`: Whether the class itself is `object` (affects policy filtering) - /// - /// Returns `ClassMemberResult::TypedDict` if a `TypedDict` base is encountered, - /// allowing the caller to handle this case specially. - /// - /// If we encounter a dynamic type in the MRO, we save it and after traversal: - /// 1. Use it as the type if no other classes define the attribute, or - /// 2. Intersect it with the type from non-dynamic MRO members. - pub(super) fn class_member( - self, - name: &str, - policy: MemberLookupPolicy, - inherited_generic_context: Option>, - is_self_object: bool, - ) -> ClassMemberResult<'db> { - let db = self.db; - let mut dynamic_type: Option> = None; - let mut lookup_result: LookupResult<'db> = - Err(LookupError::Undefined(TypeQualifiers::empty())); - - for superclass in self.mro_iter { - match superclass { - ClassBase::Generic | ClassBase::Protocol => { - // Skip over these very special class bases that aren't really classes. - } - ClassBase::Dynamic(_) => { - // Note: calling `Type::from(superclass).member()` would be incorrect here. - // What we'd really want is a `Type::Any.own_class_member()` method, - // but adding such a method wouldn't make much sense -- it would always return `Any`! - dynamic_type.get_or_insert(Type::from(superclass)); - } - ClassBase::Class(class) => { - let known = class.known(db); - - // Only exclude `object` members if this is not an `object` class itself - if known == Some(KnownClass::Object) - && policy.mro_no_object_fallback() - && !is_self_object - { - continue; - } - - if known == Some(KnownClass::Type) && policy.meta_class_no_type_fallback() { - continue; - } - - if matches!(known, Some(KnownClass::Int | KnownClass::Str)) - && policy.mro_no_int_or_str_fallback() - { - continue; - } - - lookup_result = lookup_result.or_else(|lookup_error| { - lookup_error.or_fall_back_to( - db, - class - .own_class_member(db, inherited_generic_context, name) - .inner, - ) - }); - } - ClassBase::TypedDict => { - return ClassMemberResult::TypedDict; - } - } - if lookup_result.is_ok() { - break; - } - } - - ClassMemberResult::Done(CompletedMemberLookup { - lookup_result, - dynamic_type, - }) - } - - /// Look up an instance member by iterating through the MRO. - /// - /// Unlike class member lookup, instance member lookup: - /// - Uses `own_instance_member` to check each class - /// - Builds a union of inferred types from multiple classes - /// - Stops on the first definitely-declared attribute - /// - /// Returns `InstanceMemberResult::TypedDict` if a `TypedDict` base is encountered, - /// allowing the caller to handle this case specially. - pub(super) fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { - let db = self.db; - let mut union = UnionBuilder::new(db); - let mut union_qualifiers = TypeQualifiers::empty(); - let mut is_definitely_bound = false; - - for superclass in self.mro_iter { - match superclass { - ClassBase::Generic | ClassBase::Protocol => { - // Skip over these very special class bases that aren't really classes. - } - ClassBase::Dynamic(_) => { - // We already return the dynamic type for class member lookup, so we can - // just return unbound here (to avoid having to build a union of the - // dynamic type with itself). - return InstanceMemberResult::Done(PlaceAndQualifiers::unbound()); - } - ClassBase::Class(class) => { - if let member @ PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - ty, - origin, - definedness: boundness, - .. - }), - qualifiers, - } = class.own_instance_member(db, name).inner - { - if boundness == Definedness::AlwaysDefined { - if origin.is_declared() { - // We found a definitely-declared attribute. Discard possibly collected - // inferred types from subclasses and return the declared type. - return InstanceMemberResult::Done(member); - } - - is_definitely_bound = true; - } - - // If the attribute is not definitely declared on this class, keep looking - // higher up in the MRO, and build a union of all inferred types (and - // possibly-declared types): - union = union.add(ty); - - // TODO: We could raise a diagnostic here if there are conflicting type - // qualifiers - union_qualifiers |= qualifiers; - } - } - ClassBase::TypedDict => { - return InstanceMemberResult::TypedDict; - } + // TODO: We could raise a diagnostic here if there are conflicting type + // qualifiers + union_qualifiers |= qualifiers; + } + } + ClassBase::TypedDict => { + return InstanceMemberResult::TypedDict; + } } } @@ -6458,22 +2328,6 @@ impl std::fmt::Display for QualifiedClassName<'_> { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] -pub(super) enum InheritanceCycle { - /// The class is cyclically defined and is a participant in the cycle. - /// i.e., it inherits either directly or indirectly from itself. - Participant, - /// The class inherits from a class that is a `Participant` in an inheritance cycle, - /// but is not itself a participant. - Inherited, -} - -impl InheritanceCycle { - pub(super) const fn is_participant(self) -> bool { - matches!(self, InheritanceCycle::Participant) - } -} - /// CPython internally considers a class a "solid base" if it has an atypical instance memory layout, /// with additional memory "slots" for each instance, besides the default object metadata and an /// attribute dictionary. Per [PEP 800], however, we use the term "disjoint base" for this concept. @@ -6539,1769 +2393,6 @@ pub(super) enum DisjointBaseKind { DefinesSlots, } -/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow -/// for easier syntax when interacting with very common classes. -/// -/// Feel free to expand this enum if you ever find yourself using the same class in multiple -/// places. -/// Note: good candidates are any classes in `[ty_module_resolver::module::KnownModule]` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] -#[cfg_attr(test, derive(strum_macros::EnumIter))] -pub enum KnownClass { - // To figure out where an stdlib symbol is defined, you can go into `crates/ty_vendored` - // and grep for the symbol name in any `.pyi` file. - - // Builtins - Bool, - Object, - Bytes, - Bytearray, - Type, - Int, - Float, - Complex, - Str, - List, - Tuple, - Set, - FrozenSet, - Dict, - Slice, - Property, - BaseException, - Exception, - BaseExceptionGroup, - ExceptionGroup, - Staticmethod, - Classmethod, - Super, - NotImplementedError, - // enum - Enum, - EnumType, - Auto, - Member, - Nonmember, - StrEnum, - // abc - ABCMeta, - // Types - GenericAlias, - ModuleType, - FunctionType, - MethodType, - MethodWrapperType, - WrapperDescriptorType, - UnionType, - GeneratorType, - AsyncGeneratorType, - CoroutineType, - NotImplementedType, - BuiltinFunctionType, - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - EllipsisType, - // Typeshed - NoneType, // Part of `types` for Python >= 3.10 - // Typing - Awaitable, - Generator, - Deprecated, - StdlibAlias, - SpecialForm, - TypeVar, - ParamSpec, - // typing_extensions.ParamSpec - ExtensionsParamSpec, // must be distinct from typing.ParamSpec, backports new features - ParamSpecArgs, - ParamSpecKwargs, - ProtocolMeta, - TypeVarTuple, - TypeAliasType, - NoDefaultType, - NewType, - SupportsIndex, - Iterable, - Iterator, - Sequence, - Mapping, - // typing_extensions - ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features - // Collections - ChainMap, - Counter, - DefaultDict, - Deque, - OrderedDict, - // sys - VersionInfo, - // dataclasses - Field, - KwOnly, - InitVar, - // _typeshed._type_checker_internals - NamedTupleFallback, - NamedTupleLike, - TypedDictFallback, - // string.templatelib - Template, - // pathlib - Path, - // ty_extensions - ConstraintSet, - GenericContext, - Specialization, -} - -impl KnownClass { - pub(crate) const fn is_bool(self) -> bool { - matches!(self, Self::Bool) - } - - pub(crate) const fn is_special_form(self) -> bool { - matches!(self, Self::SpecialForm) - } - - /// Determine whether instances of this class are always truthy, always falsy, - /// or have an ambiguous truthiness. - /// - /// Returns `None` for `KnownClass::Tuple`, since the truthiness of a tuple - /// depends on its spec. - pub(crate) const fn bool(self) -> Option { - match self { - // N.B. It's only generally safe to infer `Truthiness::AlwaysTrue` for a `KnownClass` - // variant if the class's `__bool__` method always returns the same thing *and* the - // class is `@final`. - // - // E.g. `ModuleType.__bool__` always returns `True`, but `ModuleType` is not `@final`. - // Equally, `range` is `@final`, but its `__bool__` method can return `False`. - Self::EllipsisType - | Self::NoDefaultType - | Self::MethodType - | Self::Slice - | Self::FunctionType - | Self::VersionInfo - | Self::TypeAliasType - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Super - | Self::WrapperDescriptorType - | Self::UnionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::MethodWrapperType - | Self::CoroutineType - | Self::BuiltinFunctionType - | Self::Template - | Self::Path => Some(Truthiness::AlwaysTrue), - - Self::NoneType => Some(Truthiness::AlwaysFalse), - - Self::BaseException - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Object - | Self::OrderedDict - | Self::BaseExceptionGroup - | Self::Bool - | Self::Str - | Self::List - | Self::GenericAlias - | Self::NewType - | Self::StdlibAlias - | Self::SupportsIndex - | Self::Set - | Self::Int - | Self::Type - | Self::Bytes - | Self::Bytearray - | Self::FrozenSet - | Self::Property - | Self::SpecialForm - | Self::Dict - | Self::ModuleType - | Self::ChainMap - | Self::Complex - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::Float - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 - // and raises a `TypeError` in Python >=3.14 - // (see https://docs.python.org/3/library/constants.html#NotImplemented) - | Self::NotImplementedType - | Self::Staticmethod - | Self::Classmethod - | Self::Awaitable - | Self::Generator - | Self::Deprecated - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::ProtocolMeta - | Self::TypedDictFallback => Some(Truthiness::Ambiguous), - - Self::Tuple => None, - } - } - - /// Return `true` if this class is a subclass of `enum.Enum` *and* has enum members, i.e. - /// if it is an "actual" enum, not `enum.Enum` itself or a similar custom enum class. - pub(crate) const fn is_enum_subclass_with_members(self) -> bool { - match self { - KnownClass::Bool - | KnownClass::Object - | KnownClass::Bytes - | KnownClass::Bytearray - | KnownClass::Type - | KnownClass::Int - | KnownClass::Float - | KnownClass::Complex - | KnownClass::Str - | KnownClass::List - | KnownClass::Tuple - | KnownClass::Set - | KnownClass::FrozenSet - | KnownClass::Dict - | KnownClass::Slice - | KnownClass::Property - | KnownClass::BaseException - | KnownClass::NotImplementedError - | KnownClass::Exception - | KnownClass::BaseExceptionGroup - | KnownClass::ExceptionGroup - | KnownClass::Staticmethod - | KnownClass::Classmethod - | KnownClass::Awaitable - | KnownClass::Generator - | KnownClass::Deprecated - | KnownClass::Super - | KnownClass::Enum - | KnownClass::EnumType - | KnownClass::Auto - | KnownClass::Member - | KnownClass::Nonmember - | KnownClass::StrEnum - | KnownClass::ABCMeta - | KnownClass::GenericAlias - | KnownClass::ModuleType - | KnownClass::FunctionType - | KnownClass::MethodType - | KnownClass::MethodWrapperType - | KnownClass::WrapperDescriptorType - | KnownClass::UnionType - | KnownClass::GeneratorType - | KnownClass::AsyncGeneratorType - | KnownClass::CoroutineType - | KnownClass::NoneType - | KnownClass::StdlibAlias - | KnownClass::SpecialForm - | KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar - | KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec - | KnownClass::ParamSpecArgs - | KnownClass::ParamSpecKwargs - | KnownClass::TypeVarTuple - | KnownClass::TypeAliasType - | KnownClass::NoDefaultType - | KnownClass::NewType - | KnownClass::SupportsIndex - | KnownClass::Iterable - | KnownClass::Iterator - | KnownClass::Sequence - | KnownClass::Mapping - | KnownClass::ChainMap - | KnownClass::Counter - | KnownClass::DefaultDict - | KnownClass::Deque - | KnownClass::OrderedDict - | KnownClass::VersionInfo - | KnownClass::EllipsisType - | KnownClass::NotImplementedType - | KnownClass::Field - | KnownClass::KwOnly - | KnownClass::InitVar - | KnownClass::NamedTupleFallback - | KnownClass::NamedTupleLike - | KnownClass::ConstraintSet - | KnownClass::GenericContext - | KnownClass::Specialization - | KnownClass::TypedDictFallback - | KnownClass::BuiltinFunctionType - | KnownClass::ProtocolMeta - | KnownClass::Template - | KnownClass::Path => false, - } - } - - /// Return `true` if this class is a (true) subclass of `typing.TypedDict`. - pub(crate) const fn is_typed_dict_subclass(self) -> bool { - match self { - KnownClass::Bool - | KnownClass::Object - | KnownClass::Bytes - | KnownClass::Bytearray - | KnownClass::Type - | KnownClass::Int - | KnownClass::Float - | KnownClass::Complex - | KnownClass::Str - | KnownClass::List - | KnownClass::Tuple - | KnownClass::Set - | KnownClass::FrozenSet - | KnownClass::Dict - | KnownClass::Slice - | KnownClass::Property - | KnownClass::BaseException - | KnownClass::Exception - | KnownClass::NotImplementedError - | KnownClass::BaseExceptionGroup - | KnownClass::ExceptionGroup - | KnownClass::Staticmethod - | KnownClass::Classmethod - | KnownClass::Awaitable - | KnownClass::Generator - | KnownClass::Deprecated - | KnownClass::Super - | KnownClass::Enum - | KnownClass::EnumType - | KnownClass::Auto - | KnownClass::Member - | KnownClass::Nonmember - | KnownClass::StrEnum - | KnownClass::ABCMeta - | KnownClass::GenericAlias - | KnownClass::ModuleType - | KnownClass::FunctionType - | KnownClass::MethodType - | KnownClass::MethodWrapperType - | KnownClass::WrapperDescriptorType - | KnownClass::UnionType - | KnownClass::GeneratorType - | KnownClass::AsyncGeneratorType - | KnownClass::CoroutineType - | KnownClass::NoneType - | KnownClass::StdlibAlias - | KnownClass::SpecialForm - | KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar - | KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec - | KnownClass::ParamSpecArgs - | KnownClass::ParamSpecKwargs - | KnownClass::TypeVarTuple - | KnownClass::TypeAliasType - | KnownClass::NoDefaultType - | KnownClass::NewType - | KnownClass::SupportsIndex - | KnownClass::Iterable - | KnownClass::Iterator - | KnownClass::Sequence - | KnownClass::Mapping - | KnownClass::ChainMap - | KnownClass::Counter - | KnownClass::DefaultDict - | KnownClass::Deque - | KnownClass::OrderedDict - | KnownClass::VersionInfo - | KnownClass::EllipsisType - | KnownClass::NotImplementedType - | KnownClass::Field - | KnownClass::KwOnly - | KnownClass::InitVar - | KnownClass::NamedTupleFallback - | KnownClass::NamedTupleLike - | KnownClass::ConstraintSet - | KnownClass::GenericContext - | KnownClass::Specialization - | KnownClass::TypedDictFallback - | KnownClass::BuiltinFunctionType - | KnownClass::ProtocolMeta - | KnownClass::Template - | KnownClass::Path => false, - } - } - - pub(crate) const fn is_tuple_subclass(self) -> bool { - match self { - KnownClass::Tuple | KnownClass::VersionInfo => true, - - KnownClass::Bool - | KnownClass::Object - | KnownClass::Bytes - | KnownClass::Bytearray - | KnownClass::Type - | KnownClass::Int - | KnownClass::Float - | KnownClass::Complex - | KnownClass::Str - | KnownClass::List - | KnownClass::Set - | KnownClass::FrozenSet - | KnownClass::Dict - | KnownClass::Slice - | KnownClass::Property - | KnownClass::BaseException - | KnownClass::Exception - | KnownClass::NotImplementedError - | KnownClass::BaseExceptionGroup - | KnownClass::ExceptionGroup - | KnownClass::Staticmethod - | KnownClass::Classmethod - | KnownClass::Awaitable - | KnownClass::Generator - | KnownClass::Deprecated - | KnownClass::Super - | KnownClass::Enum - | KnownClass::EnumType - | KnownClass::Auto - | KnownClass::Member - | KnownClass::Nonmember - | KnownClass::StrEnum - | KnownClass::ABCMeta - | KnownClass::GenericAlias - | KnownClass::ModuleType - | KnownClass::FunctionType - | KnownClass::MethodType - | KnownClass::MethodWrapperType - | KnownClass::WrapperDescriptorType - | KnownClass::UnionType - | KnownClass::GeneratorType - | KnownClass::AsyncGeneratorType - | KnownClass::CoroutineType - | KnownClass::NoneType - | KnownClass::StdlibAlias - | KnownClass::SpecialForm - | KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar - | KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec - | KnownClass::ParamSpecArgs - | KnownClass::ParamSpecKwargs - | KnownClass::TypeVarTuple - | KnownClass::TypeAliasType - | KnownClass::NoDefaultType - | KnownClass::NewType - | KnownClass::SupportsIndex - | KnownClass::Iterable - | KnownClass::Iterator - | KnownClass::Sequence - | KnownClass::Mapping - | KnownClass::ChainMap - | KnownClass::Counter - | KnownClass::DefaultDict - | KnownClass::Deque - | KnownClass::OrderedDict - | KnownClass::EllipsisType - | KnownClass::NotImplementedType - | KnownClass::Field - | KnownClass::KwOnly - | KnownClass::InitVar - | KnownClass::TypedDictFallback - | KnownClass::NamedTupleLike - | KnownClass::NamedTupleFallback - | KnownClass::ConstraintSet - | KnownClass::GenericContext - | KnownClass::Specialization - | KnownClass::BuiltinFunctionType - | KnownClass::ProtocolMeta - | KnownClass::Template - | KnownClass::Path => false, - } - } - - /// Return `true` if this class is a protocol class. - /// - /// In an ideal world, perhaps we wouldn't hardcode this knowledge here; - /// instead, we'd just look at the bases for these classes, as we do for - /// all other classes. However, the special casing here helps us out in - /// two important ways: - /// - /// 1. It helps us avoid Salsa cycles when creating types such as "instance of `str`" - /// and "instance of `sys._version_info`". These types are constructed very early - /// on, but it causes problems if we attempt to infer the types of their bases - /// too soon. - /// 2. It's probably more performant. - const fn is_protocol(self) -> bool { - match self { - Self::SupportsIndex - | Self::Iterable - | Self::Iterator - | Self::Awaitable - | Self::NamedTupleLike - | Self::Generator => true, - - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Tuple - | Self::Int - | Self::Float - | Self::Complex - | Self::FrozenSet - | Self::Str - | Self::Set - | Self::Dict - | Self::List - | Self::Type - | Self::Slice - | Self::Property - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Deprecated - | Self::GenericAlias - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::NoneType - | Self::SpecialForm - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::TypeAliasType - | Self::NoDefaultType - | Self::NewType - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Super - | Self::StdlibAlias - | Self::VersionInfo - | Self::EllipsisType - | Self::NotImplementedType - | Self::UnionType - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::NamedTupleFallback - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::TypedDictFallback - | Self::BuiltinFunctionType - | Self::ProtocolMeta - | Self::Template - | Self::Path - | Self::Mapping - | Self::Sequence => false, - } - } - - /// Return `true` if this class is a typeshed fallback class which is used to provide attributes and - /// methods for another type (e.g. `NamedTupleFallback` for actual `NamedTuple`s). These fallback - /// classes need special treatment in some places. For example, implicit usages of `Self` should not - /// be eagerly replaced with the fallback class itself. Instead, `Self` should eventually be treated - /// as referring to the destination type (e.g. the actual `NamedTuple`). - pub(crate) const fn is_fallback_class(self) -> bool { - match self { - KnownClass::Bool - | KnownClass::Object - | KnownClass::Bytes - | KnownClass::Bytearray - | KnownClass::Type - | KnownClass::Int - | KnownClass::Float - | KnownClass::Complex - | KnownClass::Str - | KnownClass::List - | KnownClass::Tuple - | KnownClass::Set - | KnownClass::FrozenSet - | KnownClass::Dict - | KnownClass::Slice - | KnownClass::Property - | KnownClass::BaseException - | KnownClass::Exception - | KnownClass::NotImplementedError - | KnownClass::BaseExceptionGroup - | KnownClass::ExceptionGroup - | KnownClass::Staticmethod - | KnownClass::Classmethod - | KnownClass::Super - | KnownClass::Enum - | KnownClass::EnumType - | KnownClass::Auto - | KnownClass::Member - | KnownClass::Nonmember - | KnownClass::StrEnum - | KnownClass::ABCMeta - | KnownClass::GenericAlias - | KnownClass::ModuleType - | KnownClass::FunctionType - | KnownClass::MethodType - | KnownClass::MethodWrapperType - | KnownClass::WrapperDescriptorType - | KnownClass::UnionType - | KnownClass::GeneratorType - | KnownClass::AsyncGeneratorType - | KnownClass::CoroutineType - | KnownClass::NotImplementedType - | KnownClass::BuiltinFunctionType - | KnownClass::EllipsisType - | KnownClass::NoneType - | KnownClass::Awaitable - | KnownClass::Generator - | KnownClass::Deprecated - | KnownClass::StdlibAlias - | KnownClass::SpecialForm - | KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar - | KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec - | KnownClass::ParamSpecArgs - | KnownClass::ParamSpecKwargs - | KnownClass::ProtocolMeta - | KnownClass::TypeVarTuple - | KnownClass::TypeAliasType - | KnownClass::NoDefaultType - | KnownClass::NewType - | KnownClass::SupportsIndex - | KnownClass::Iterable - | KnownClass::Iterator - | KnownClass::Sequence - | KnownClass::Mapping - | KnownClass::ChainMap - | KnownClass::Counter - | KnownClass::DefaultDict - | KnownClass::Deque - | KnownClass::OrderedDict - | KnownClass::VersionInfo - | KnownClass::Field - | KnownClass::KwOnly - | KnownClass::NamedTupleLike - | KnownClass::Template - | KnownClass::Path - | KnownClass::ConstraintSet - | KnownClass::GenericContext - | KnownClass::Specialization - | KnownClass::InitVar => false, - KnownClass::NamedTupleFallback | KnownClass::TypedDictFallback => true, - } - } - - pub(crate) fn name(self, db: &dyn Db) -> &'static str { - match self { - Self::Bool => "bool", - Self::Object => "object", - Self::Bytes => "bytes", - Self::Bytearray => "bytearray", - Self::Tuple => "tuple", - Self::Int => "int", - Self::Float => "float", - Self::Complex => "complex", - Self::FrozenSet => "frozenset", - Self::Str => "str", - Self::Set => "set", - Self::Dict => "dict", - Self::List => "list", - Self::Type => "type", - Self::Slice => "slice", - Self::Property => "property", - Self::BaseException => "BaseException", - Self::BaseExceptionGroup => "BaseExceptionGroup", - Self::Exception => "Exception", - Self::NotImplementedError => "NotImplementedError", - Self::ExceptionGroup => "ExceptionGroup", - Self::Staticmethod => "staticmethod", - Self::Classmethod => "classmethod", - Self::Awaitable => "Awaitable", - Self::Generator => "Generator", - Self::Deprecated => "deprecated", - Self::GenericAlias => "GenericAlias", - Self::ModuleType => "ModuleType", - Self::FunctionType => "FunctionType", - Self::MethodType => "MethodType", - Self::UnionType => "UnionType", - Self::MethodWrapperType => "MethodWrapperType", - Self::WrapperDescriptorType => "WrapperDescriptorType", - Self::BuiltinFunctionType => "BuiltinFunctionType", - Self::GeneratorType => "GeneratorType", - Self::AsyncGeneratorType => "AsyncGeneratorType", - Self::CoroutineType => "CoroutineType", - Self::NoneType => "NoneType", - Self::SpecialForm => "_SpecialForm", - Self::TypeVar => "TypeVar", - Self::ExtensionsTypeVar => "TypeVar", - Self::ParamSpec => "ParamSpec", - Self::ExtensionsParamSpec => "ParamSpec", - Self::ParamSpecArgs => "ParamSpecArgs", - Self::ParamSpecKwargs => "ParamSpecKwargs", - Self::TypeVarTuple => "TypeVarTuple", - Self::TypeAliasType => "TypeAliasType", - Self::NoDefaultType => "_NoDefaultType", - Self::NewType => "NewType", - Self::SupportsIndex => "SupportsIndex", - Self::ChainMap => "ChainMap", - Self::Counter => "Counter", - Self::DefaultDict => "defaultdict", - Self::Deque => "deque", - Self::OrderedDict => "OrderedDict", - Self::Enum => "Enum", - Self::EnumType => { - if Program::get(db).python_version(db) >= PythonVersion::PY311 { - "EnumType" - } else { - "EnumMeta" - } - } - Self::Auto => "auto", - Self::Member => "member", - Self::Nonmember => "nonmember", - Self::StrEnum => "StrEnum", - Self::ABCMeta => "ABCMeta", - Self::Super => "super", - Self::Iterable => "Iterable", - Self::Iterator => "Iterator", - Self::Sequence => "Sequence", - Self::Mapping => "Mapping", - // For example, `typing.List` is defined as `List = _Alias()` in typeshed - Self::StdlibAlias => "_Alias", - // This is the name the type of `sys.version_info` has in typeshed, - // which is different to what `type(sys.version_info).__name__` is at runtime. - // (At runtime, `type(sys.version_info).__name__ == "version_info"`, - // which is impossible to replicate in the stubs since the sole instance of the class - // also has that name in the `sys` module.) - Self::VersionInfo => "_version_info", - Self::EllipsisType => { - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - "EllipsisType" - } else { - "ellipsis" - } - } - Self::NotImplementedType => { - // Exposed as `types.NotImplementedType` on Python >=3.10; - // backported as `builtins._NotImplementedType` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - "NotImplementedType" - } else { - "_NotImplementedType" - } - } - Self::Field => "Field", - Self::KwOnly => "KW_ONLY", - Self::InitVar => "InitVar", - Self::NamedTupleFallback => "NamedTupleFallback", - Self::NamedTupleLike => "NamedTupleLike", - Self::ConstraintSet => "ConstraintSet", - Self::GenericContext => "GenericContext", - Self::Specialization => "Specialization", - Self::TypedDictFallback => "TypedDictFallback", - Self::Template => "Template", - Self::Path => "Path", - Self::ProtocolMeta => "_ProtocolMeta", - } - } - - pub(super) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { - struct KnownClassDisplay<'db> { - db: &'db dyn Db, - class: KnownClass, - } - - impl std::fmt::Display for KnownClassDisplay<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let KnownClassDisplay { - class: known_class, - db, - } = *self; - write!( - f, - "{module}.{class}", - module = known_class.canonical_module(db), - class = known_class.name(db) - ) - } - } - - KnownClassDisplay { db, class: self } - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing all possible instances of - /// the class. If this class is generic, this will use the default specialization. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - #[track_caller] - pub fn to_instance(self, db: &dyn Db) -> Type<'_> { - debug_assert_ne!( - self, - KnownClass::Tuple, - "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" - ); - self.to_class_literal(db) - .to_class_type(db) - .map(|class| Type::instance(db, class)) - .unwrap_or_else(Type::unknown) - } - - /// Similar to [`KnownClass::to_instance`], but returns the Unknown-specialization where each type - /// parameter is specialized to `Unknown`. - #[track_caller] - pub(crate) fn to_instance_unknown(self, db: &dyn Db) -> Type<'_> { - debug_assert_ne!( - self, - KnownClass::Tuple, - "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" - ); - self.try_to_class_literal(db) - .map(|literal| Type::instance(db, literal.unknown_specialization(db))) - .unwrap_or_else(Type::unknown) - } - - /// Lookup a generic [`KnownClass`] in typeshed and return a [`Type`] - /// representing a specialization of that class. - /// - /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong - /// number of types, a debug-level log message will be emitted stating this. - pub(crate) fn to_specialized_class_type<'t, 'db, T>( - self, - db: &'db dyn Db, - specialization: T, - ) -> Option> - where - T: Into]>>, - 'db: 't, - { - fn to_specialized_class_type_impl<'db>( - db: &'db dyn Db, - class: KnownClass, - class_literal: StaticClassLiteral<'db>, - specialization: Cow<[Type<'db>]>, - generic_context: GenericContext<'db>, - ) -> ClassType<'db> { - if specialization.len() != generic_context.len(db) { - // a cache of the `KnownClass`es that we have already seen mismatched-arity - // specializations for (and therefore that we've already logged a warning for) - static MESSAGES: LazyLock>> = - LazyLock::new(Mutex::default); - if MESSAGES.lock().unwrap().insert(class) { - tracing::info!( - "Wrong number of types when specializing {}. \ - Falling back to default specialization for the symbol instead.", - class.display(db) - ); - } - return class_literal.default_specialization(db); - } - - class_literal - .apply_specialization(db, |_| generic_context.specialize(db, specialization)) - } - - let class_literal = self.to_class_literal(db).as_class_literal()?.as_static()?; - let generic_context = class_literal.generic_context(db)?; - let specialization = specialization.into(); - - Some(to_specialized_class_type_impl( - db, - self, - class_literal, - specialization, - generic_context, - )) - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] - /// representing all possible instances of the generic class with a specialization. - /// - /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong - /// number of types, a debug-level log message will be emitted stating this. - #[track_caller] - pub(crate) fn to_specialized_instance<'t, 'db, T>( - self, - db: &'db dyn Db, - specialization: T, - ) -> Type<'db> - where - T: Into]>>, - 'db: 't, - { - debug_assert_ne!( - self, - KnownClass::Tuple, - "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" - ); - self.to_specialized_class_type(db, specialization) - .and_then(|class_type| Type::from(class_type).to_instance(db)) - .unwrap_or_else(Type::unknown) - } - - /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. - /// - /// Return an error if the symbol cannot be found in the expected typeshed module, - /// or if the symbol is not a class definition, or if the symbol is possibly unbound. - fn try_to_class_literal_without_logging( - self, - db: &dyn Db, - ) -> Result, KnownClassLookupError<'_>> { - let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place; - match symbol { - Place::Defined(DefinedPlace { - ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), - definedness: Definedness::AlwaysDefined, - .. - }) => Ok(class_literal), - Place::Defined(DefinedPlace { - ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), - definedness: Definedness::PossiblyUndefined, - .. - }) => Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }), - Place::Defined(DefinedPlace { ty: found_type, .. }) => { - Err(KnownClassLookupError::SymbolNotAClass { found_type }) - } - Place::Undefined => Err(KnownClassLookupError::ClassNotFound), - } - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> { - #[salsa::interned(heap_size=ruff_memory_usage::heap_size)] - struct KnownClassArgument { - class: KnownClass, - } - - fn known_class_to_class_literal_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _class: KnownClassArgument<'db>, - ) -> Option> { - None - } - - #[salsa::tracked(cycle_initial=known_class_to_class_literal_initial, heap_size=ruff_memory_usage::heap_size)] - fn known_class_to_class_literal<'db>( - db: &'db dyn Db, - class: KnownClassArgument<'db>, - ) -> Option> { - let class = class.class(db); - class - .try_to_class_literal_without_logging(db) - .or_else(|lookup_error| { - if matches!( - lookup_error, - KnownClassLookupError::ClassPossiblyUnbound { .. } - ) { - tracing::info!("{}", lookup_error.display(db, class)); - } else { - tracing::info!( - "{}. Falling back to `Unknown` for the symbol instead.", - lookup_error.display(db, class) - ); - } - - match lookup_error { - KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { - Ok(class_literal) - } - KnownClassLookupError::ClassNotFound { .. } - | KnownClassLookupError::SymbolNotAClass { .. } => Err(()), - } - }) - .ok() - } - - known_class_to_class_literal(db, KnownClassArgument::new(db, self)) - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type<'_> { - self.try_to_class_literal(db) - .map(|class| Type::ClassLiteral(ClassLiteral::Static(class))) - .unwrap_or_else(Type::unknown) - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] - /// representing that class and all possible subclasses of the class. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub fn to_subclass_of(self, db: &dyn Db) -> Type<'_> { - self.to_class_literal(db) - .to_class_type(db) - .map(|class| SubclassOfType::from(db, class)) - .unwrap_or_else(SubclassOfType::subclass_of_unknown) - } - - /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, - /// *and* `class` is a subclass of `other`. - pub(super) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.try_to_class_literal_without_logging(db) - .is_ok_and(|class| class.is_subclass_of(db, None, other)) - } - - pub(super) fn when_subclass_of<'db, 'c>( - self, - db: &'db dyn Db, - other: ClassType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - ) -> ConstraintSet<'db, 'c> { - ConstraintSet::from_bool(constraints, self.is_subclass_of(db, other)) - } - - /// Return the module in which we should look up the definition for this class - pub(super) fn canonical_module(self, db: &dyn Db) -> KnownModule { - match self { - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Tuple - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Slice - | Self::Super - | Self::Property => KnownModule::Builtins, - Self::VersionInfo => KnownModule::Sys, - Self::ABCMeta => KnownModule::Abc, - Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum => KnownModule::Enum, - Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::MethodWrapperType - | Self::UnionType - | Self::BuiltinFunctionType - | Self::WrapperDescriptorType => KnownModule::Types, - Self::NoneType => KnownModule::Typeshed, - Self::Awaitable - | Self::Generator - | Self::SpecialForm - | Self::TypeVar - | Self::StdlibAlias - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - | Self::ProtocolMeta - | Self::SupportsIndex => KnownModule::Typing, - Self::TypeAliasType - | Self::ExtensionsTypeVar - | Self::TypeVarTuple - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::Deprecated - | Self::NewType => KnownModule::TypingExtensions, - Self::ParamSpec => { - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - KnownModule::Typing - } else { - KnownModule::TypingExtensions - } - } - Self::NoDefaultType => { - let python_version = Program::get(db).python_version(db); - - // typing_extensions has a 3.13+ re-export for the `typing.NoDefault` - // singleton, but not for `typing._NoDefaultType`. So we need to switch - // to `typing._NoDefaultType` for newer versions: - if python_version >= PythonVersion::PY313 { - KnownModule::Typing - } else { - KnownModule::TypingExtensions - } - } - Self::EllipsisType => { - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - KnownModule::Types - } else { - KnownModule::Builtins - } - } - Self::NotImplementedType => { - // Exposed as `types.NotImplementedType` on Python >=3.10; - // backported as `builtins._NotImplementedType` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - KnownModule::Types - } else { - KnownModule::Builtins - } - } - Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict => KnownModule::Collections, - Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses, - Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals, - Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization => KnownModule::TyExtensions, - Self::Template => KnownModule::Templatelib, - Self::Path => KnownModule::Pathlib, - } - } - - /// Returns `Some(true)` if all instances of this `KnownClass` compare equal. - /// Returns `None` for `KnownClass::Tuple`, since whether or not a tuple type - /// is single-valued depends on the tuple spec. - pub(super) const fn is_single_valued(self) -> Option { - match self { - Self::NoneType - | Self::NoDefaultType - | Self::VersionInfo - | Self::EllipsisType - | Self::TypeAliasType - | Self::UnionType - | Self::NotImplementedType => Some(true), - - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::Slice - | Self::Property - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Awaitable - | Self::Generator - | Self::Deprecated - | Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::SpecialForm - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::SupportsIndex - | Self::StdlibAlias - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Super - | Self::NewType - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::TypedDictFallback - | Self::BuiltinFunctionType - | Self::ProtocolMeta - | Self::Template - | Self::Path => Some(false), - - Self::Tuple => None, - } - } - - /// Is this class a singleton class? - /// - /// A singleton class is a class where it is known that only one instance can ever exist at runtime. - pub(super) const fn is_singleton(self) -> bool { - match self { - Self::NoneType - | Self::EllipsisType - | Self::NoDefaultType - | Self::VersionInfo - | Self::TypeAliasType - | Self::NotImplementedType => true, - - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Tuple - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::List - | Self::Type - | Self::Slice - | Self::Property - | Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::SpecialForm - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::StdlibAlias - | Self::SupportsIndex - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Awaitable - | Self::Generator - | Self::Deprecated - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Super - | Self::UnionType - | Self::NewType - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::TypedDictFallback - | Self::BuiltinFunctionType - | Self::ProtocolMeta - | Self::Template - | Self::Path => false, - } - } - - pub(super) fn try_from_file_and_name( - db: &dyn Db, - file: File, - class_name: &str, - ) -> Option { - // We assert that this match is exhaustive over the right-hand side in the unit test - // `known_class_roundtrip_from_str()` - let candidates: &[Self] = match class_name { - "bool" => &[Self::Bool], - "object" => &[Self::Object], - "bytes" => &[Self::Bytes], - "bytearray" => &[Self::Bytearray], - "tuple" => &[Self::Tuple], - "type" => &[Self::Type], - "int" => &[Self::Int], - "float" => &[Self::Float], - "complex" => &[Self::Complex], - "str" => &[Self::Str], - "set" => &[Self::Set], - "frozenset" => &[Self::FrozenSet], - "dict" => &[Self::Dict], - "list" => &[Self::List], - "slice" => &[Self::Slice], - "property" => &[Self::Property], - "BaseException" => &[Self::BaseException], - "BaseExceptionGroup" => &[Self::BaseExceptionGroup], - "Exception" => &[Self::Exception], - "NotImplementedError" => &[Self::NotImplementedError], - "ExceptionGroup" => &[Self::ExceptionGroup], - "staticmethod" => &[Self::Staticmethod], - "classmethod" => &[Self::Classmethod], - "Awaitable" => &[Self::Awaitable], - "Generator" => &[Self::Generator], - "deprecated" => &[Self::Deprecated], - "GenericAlias" => &[Self::GenericAlias], - "NoneType" => &[Self::NoneType], - "ModuleType" => &[Self::ModuleType], - "GeneratorType" => &[Self::GeneratorType], - "AsyncGeneratorType" => &[Self::AsyncGeneratorType], - "CoroutineType" => &[Self::CoroutineType], - "FunctionType" => &[Self::FunctionType], - "MethodType" => &[Self::MethodType], - "UnionType" => &[Self::UnionType], - "MethodWrapperType" => &[Self::MethodWrapperType], - "WrapperDescriptorType" => &[Self::WrapperDescriptorType], - "BuiltinFunctionType" => &[Self::BuiltinFunctionType], - "NewType" => &[Self::NewType], - "TypeAliasType" => &[Self::TypeAliasType], - "TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar], - "Iterable" => &[Self::Iterable], - "Iterator" => &[Self::Iterator], - "Sequence" => &[Self::Sequence], - "Mapping" => &[Self::Mapping], - "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec], - "ParamSpecArgs" => &[Self::ParamSpecArgs], - "ParamSpecKwargs" => &[Self::ParamSpecKwargs], - "TypeVarTuple" => &[Self::TypeVarTuple], - "ChainMap" => &[Self::ChainMap], - "Counter" => &[Self::Counter], - "defaultdict" => &[Self::DefaultDict], - "deque" => &[Self::Deque], - "OrderedDict" => &[Self::OrderedDict], - "_Alias" => &[Self::StdlibAlias], - "_SpecialForm" => &[Self::SpecialForm], - "_NoDefaultType" => &[Self::NoDefaultType], - "SupportsIndex" => &[Self::SupportsIndex], - "Enum" => &[Self::Enum], - "EnumMeta" => &[Self::EnumType], - "EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - &[Self::EnumType] - } - "StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - &[Self::StrEnum] - } - "auto" => &[Self::Auto], - "member" => &[Self::Member], - "nonmember" => &[Self::Nonmember], - "ABCMeta" => &[Self::ABCMeta], - "super" => &[Self::Super], - "_version_info" => &[Self::VersionInfo], - "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { - &[Self::EllipsisType] - } - "EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { - &[Self::EllipsisType] - } - "_NotImplementedType" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { - &[Self::NotImplementedType] - } - "NotImplementedType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { - &[Self::NotImplementedType] - } - "Field" => &[Self::Field], - "KW_ONLY" => &[Self::KwOnly], - "InitVar" => &[Self::InitVar], - "NamedTupleFallback" => &[Self::NamedTupleFallback], - "NamedTupleLike" => &[Self::NamedTupleLike], - "ConstraintSet" => &[Self::ConstraintSet], - "GenericContext" => &[Self::GenericContext], - "Specialization" => &[Self::Specialization], - "TypedDictFallback" => &[Self::TypedDictFallback], - "Template" => &[Self::Template], - "Path" => &[Self::Path], - "_ProtocolMeta" => &[Self::ProtocolMeta], - _ => return None, - }; - - let module = file_to_module(db, file)?.known(db)?; - - candidates - .iter() - .copied() - .find(|&candidate| candidate.check_module(db, module)) - } - - /// Return `true` if the module of `self` matches `module` - fn check_module(self, db: &dyn Db, module: KnownModule) -> bool { - match self { - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Tuple - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::Slice - | Self::Property - | Self::GenericAlias - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will - | Self::ModuleType - | Self::VersionInfo - | Self::BaseException - | Self::Exception - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::EllipsisType - | Self::BaseExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::Enum - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::ABCMeta - | Self::Super - | Self::NotImplementedType - | Self::UnionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::WrapperDescriptorType - | Self::BuiltinFunctionType - | Self::Field - | Self::KwOnly - | Self::InitVar - | Self::NamedTupleFallback - | Self::TypedDictFallback - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::GenericContext - | Self::Specialization - | Self::Awaitable - | Self::Generator - | Self::Template - | Self::Path => module == self.canonical_module(db), - Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), - Self::SpecialForm - | Self::TypeAliasType - | Self::NoDefaultType - | Self::SupportsIndex - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Iterable - | Self::Iterator - | Self::Sequence - | Self::Mapping - | Self::ProtocolMeta - | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), - Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), - } - } - - /// Evaluate a call to this known class, emit any diagnostics that are necessary - /// as a result of the call, and return the type that results from the call. - pub(super) fn check_call<'db>( - self, - context: &InferContext<'db, '_>, - index: &SemanticIndex<'db>, - overload: &mut Binding<'db>, - call_expression: &ast::ExprCall, - ) { - let db = context.db(); - let scope = context.scope(); - let module = context.module(); - - match self { - KnownClass::Super => { - // Handle the case where `super()` is called with no arguments. - // In this case, we need to infer the two arguments: - // 1. The nearest enclosing class - // 2. The first parameter of the current function (typically `self` or `cls`) - match overload.parameter_types() { - [] => { - let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) - else { - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic(context, call_expression.into()); - overload.set_return_type(Type::unknown()); - return; - }; - - // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. - if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into(), None) { - if let Some(builder) = context - .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) - { - builder.into_diagnostic(format_args!( - "Cannot use `super()` in a method of NamedTuple class `{}`", - enclosing_class.name(db) - )); - } - overload.set_return_type(Type::unknown()); - return; - } - - // The type of the first parameter if the given scope is function-like (i.e. function or lambda). - // `None` if the scope is not function-like, or has no parameters. - let first_param = match scope.node(db) { - NodeWithScopeKind::Function(f) => { - f.node(module).parameters.iter().next() - } - NodeWithScopeKind::Lambda(l) => l - .node(module) - .parameters - .as_ref() - .into_iter() - .flatten() - .next(), - _ => None, - }; - - let Some(first_param) = first_param else { - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic(context, call_expression.into()); - overload.set_return_type(Type::unknown()); - return; - }; - - let definition = index.expect_single_definition(first_param); - let first_param = binding_type(db, definition); - - let bound_super = BoundSuperType::build( - db, - Type::ClassLiteral(ClassLiteral::Static(enclosing_class)), - first_param, - ) - .unwrap_or_else(|err| { - err.report_diagnostic(context, call_expression.into()); - Type::unknown() - }); - - overload.set_return_type(bound_super); - } - [Some(pivot_class_type), Some(owner_type)] => { - // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. - if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) { - if CodeGeneratorKind::NamedTuple.matches( - db, - enclosing_class.into(), - None, - ) { - if let Some(builder) = context - .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) - { - builder.into_diagnostic(format_args!( - "Cannot use `super()` in a method of NamedTuple class `{}`", - enclosing_class.name(db) - )); - } - overload.set_return_type(Type::unknown()); - return; - } - } - - let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type) - .unwrap_or_else(|err| { - err.report_diagnostic(context, call_expression.into()); - Type::unknown() - }); - overload.set_return_type(bound_super); - } - _ => {} - } - } - - KnownClass::Deprecated => { - // Parsing something of the form: - // - // @deprecated("message") - // @deprecated("message", category = DeprecationWarning, stacklevel = 1) - // - // "Static type checker behavior is not affected by the category and stacklevel arguments" - // so we only need the message and can ignore everything else. The message is mandatory, - // must be a LiteralString, and always comes first. - // - // We aren't guaranteed to know the static value of a LiteralString, so we need to - // accept that sometimes we will fail to include the message. - // - // We don't do any serious validation/diagnostics here, as the signature for this - // is included in `Type::bindings`. - // - // See: - let [Some(message), ..] = overload.parameter_types() else { - // Checking in Type::bindings will complain about this for us - return; - }; - - overload.set_return_type(Type::KnownInstance(KnownInstanceType::Deprecated( - DeprecatedInstance::new(db, message.as_string_literal()), - ))); - } - - _ => {} - } - } -} - -/// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum KnownClassLookupError<'db> { - /// There is no symbol by that name in the expected typeshed module. - ClassNotFound, - /// There is a symbol by that name in the expected typeshed module, - /// but it's not a class. - SymbolNotAClass { found_type: Type<'db> }, - /// There is a symbol by that name in the expected typeshed module, - /// and it's a class definition, but it's possibly unbound. - ClassPossiblyUnbound { - class_literal: StaticClassLiteral<'db>, - }, -} - -impl<'db> KnownClassLookupError<'db> { - fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db { - struct ErrorDisplay<'db> { - db: &'db dyn Db, - class: KnownClass, - error: KnownClassLookupError<'db>, - } - - impl std::fmt::Display for ErrorDisplay<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let ErrorDisplay { db, class, error } = *self; - - let class = class.display(db); - let python_version = Program::get(db).python_version(db); - - match error { - KnownClassLookupError::ClassNotFound => write!( - f, - "Could not find class `{class}` in typeshed on Python {python_version}", - ), - KnownClassLookupError::SymbolNotAClass { found_type } => write!( - f, - "Error looking up `{class}` in typeshed: expected to find a class definition \ - on Python {python_version}, but found a symbol of type `{found_type}` instead", - found_type = found_type.display(db), - ), - KnownClassLookupError::ClassPossiblyUnbound { .. } => write!( - f, - "Error looking up `{class}` in typeshed on Python {python_version}: \ - expected to find a fully bound symbol, but found one that is possibly unbound", - ), - } - } - } - - ErrorDisplay { - db, - class, - error: *self, - } - } -} - #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(super) struct MetaclassError<'db> { kind: MetaclassErrorKind<'db>, @@ -8392,127 +2483,3 @@ impl SlotsKind { } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::db::tests::setup_db; - use crate::{PythonVersionSource, PythonVersionWithSource}; - use salsa::Setter; - use strum::IntoEnumIterator; - use ty_module_resolver::resolve_module_confident; - - #[test] - fn known_class_roundtrip_from_str() { - let mut db = setup_db(); - Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_preview(), - source: PythonVersionSource::default(), - }); - for class in KnownClass::iter() { - let class_name = class.name(&db); - let class_module = - resolve_module_confident(&db, &class.canonical_module(&db).name()).unwrap(); - - assert_eq!( - KnownClass::try_from_file_and_name( - &db, - class_module.file(&db).unwrap(), - class_name - ), - Some(class), - "`KnownClass::candidate_from_str` appears to be missing a case for `{class_name}`" - ); - } - } - - #[test] - fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { - let mut db = setup_db(); - - Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_ty(), - source: PythonVersionSource::default(), - }); - - for class in KnownClass::iter() { - // Check the class can be looked up successfully - class.try_to_class_literal_without_logging(&db).unwrap(); - - // We can't call `KnownClass::Tuple.to_instance()`; - // there are assertions to ensure that we always call `Type::homogeneous_tuple()` - // or `Type::heterogeneous_tuple()` instead.` - if class != KnownClass::Tuple { - assert_ne!( - class.to_instance(&db), - Type::unknown(), - "Unexpectedly fell back to `Unknown` for `{class:?}`" - ); - } - } - } - - #[test] - fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { - let mut db = setup_db(); - - // First, collect the `KnownClass` variants - // and sort them according to the version they were added in. - // This makes the test far faster as it minimizes the number of times - // we need to change the Python version in the loop. - let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter() - .map(|class| { - let version_added = match class { - KnownClass::Template => PythonVersion::PY314, - KnownClass::UnionType => PythonVersion::PY310, - KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => { - PythonVersion::PY311 - } - KnownClass::GenericAlias => PythonVersion::PY39, - KnownClass::KwOnly => PythonVersion::PY310, - KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => { - PythonVersion::PY311 - } - KnownClass::ParamSpec => PythonVersion::PY310, - _ => PythonVersion::PY37, - }; - (class, version_added) - }) - .collect(); - - classes.sort_unstable_by_key(|(_, version)| *version); - - let program = Program::get(&db); - let mut current_version = program.python_version(&db); - - for (class, version_added) in classes { - if version_added != current_version { - program - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: version_added, - source: PythonVersionSource::default(), - }); - current_version = version_added; - } - - // Check the class can be looked up successfully - class.try_to_class_literal_without_logging(&db).unwrap(); - - // We can't call `KnownClass::Tuple.to_instance()`; - // there are assertions to ensure that we always call `Type::homogeneous_tuple()` - // or `Type::heterogeneous_tuple()` instead.` - if class != KnownClass::Tuple { - assert_ne!( - class.to_instance(&db), - Type::unknown(), - "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {version_added}" - ); - } - } - } -} diff --git a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs new file mode 100644 index 0000000000000..6db6793f7e37d --- /dev/null +++ b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs @@ -0,0 +1,509 @@ +use std::borrow::Cow; + +use ruff_db::{diagnostic::Span, parsed::parsed_module}; +use ruff_python_ast::{self as ast, NodeIndex, name::Name}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::{ + Db, TypeQualifiers, + place::{Place, PlaceAndQualifiers}, + semantic_index::{definition::Definition, scope::ScopeId}, + types::{ + ClassBase, ClassLiteral, ClassType, DataclassParams, KnownClass, MemberLookupPolicy, + SubclassOfType, Type, + class::{ + ClassMemberResult, CodeGeneratorKind, DisjointBase, InstanceMemberResult, MroLookup, + }, + definition_expression_type, + member::Member, + mro::{DynamicMroError, Mro, MroIterator}, + }, +}; + +/// A class created dynamically via a three-argument `type()` call. +/// +/// For example: +/// ```python +/// Foo = type("Foo", (Base,), {"attr": 1}) +/// ``` +/// +/// The type of `Foo` would be `` where `Foo` is a `DynamicClassLiteral` with: +/// - name: "Foo" +/// - members: [("attr", int)] +/// +/// This is called "dynamic" because the class is created dynamically at runtime +/// via a function call rather than a class statement. +/// +/// # Salsa interning +/// +/// This is a Salsa-interned struct. Two different `type()` calls always produce +/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases: +/// +/// ```python +/// Foo1 = type("Foo", (Base,), {}) +/// Foo2 = type("Foo", (Base,), {}) +/// # Foo1 and Foo2 are distinct types +/// ``` +/// +/// The `anchor` field provides stable identity: +/// - For assigned `type()` calls, the `Definition` uniquely identifies the class. +/// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope +/// provides stable identity that only changes when the scope itself changes. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct DynamicClassLiteral<'db> { + /// The name of the class (from the first argument to `type()`). + #[returns(ref)] + pub name: Name, + + /// The anchor for this dynamic class, providing stable identity. + /// + /// - `Definition`: The `type()` call is assigned to a variable. The definition + /// uniquely identifies this class and can be used to find the `type()` call. + /// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset + /// is relative to the enclosing scope's anchor node index. + #[returns(ref)] + pub anchor: DynamicClassAnchor<'db>, + + /// The class members from the namespace dict (third argument to `type()`). + /// Each entry is a (name, type) pair extracted from the dict literal. + #[returns(deref)] + pub members: Box<[(Name, Type<'db>)]>, + + /// Whether the namespace dict (third argument) is dynamic (not a literal dict, + /// or contains non-string-literal keys). When true, attribute lookups on this + /// class and its instances return `Unknown` instead of failing. + pub has_dynamic_namespace: bool, + + /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator + /// or passed to `dataclass()` as a function. + pub dataclass_params: Option>, +} + +/// Anchor for identifying a dynamic class literal. +/// +/// This enum provides stable identity for `DynamicClassLiteral`: +/// - For assigned calls, the `Definition` uniquely identifies the class. +/// - For dangling calls, a relative offset provides stable identity. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub enum DynamicClassAnchor<'db> { + /// The `type()` call is assigned to a variable. + /// + /// The `Definition` uniquely identifies this class. The `type()` call expression + /// is the `value` of the assignment, so we can get its range from the definition. + Definition(Definition<'db>), + + /// The `type()` call is "dangling" (not assigned to a variable). + /// + /// The offset is relative to the enclosing scope's anchor node index. + /// For module scope, this is equivalent to an absolute index (anchor is 0). + /// + /// The `explicit_bases` are computed eagerly at creation time since dangling + /// calls cannot recursively reference the class being defined. + ScopeOffset { + scope: ScopeId<'db>, + offset: u32, + explicit_bases: Box<[Type<'db>]>, + }, +} + +impl get_size2::GetSize for DynamicClassLiteral<'_> {} + +#[salsa::tracked] +impl<'db> DynamicClassLiteral<'db> { + /// Returns the definition where this class is created, if it was assigned to a variable. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + match self.anchor(db) { + DynamicClassAnchor::Definition(definition) => Some(*definition), + DynamicClassAnchor::ScopeOffset { .. } => None, + } + } + + /// Returns the scope in which this dynamic class was created. + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + match self.anchor(db) { + DynamicClassAnchor::Definition(definition) => definition.scope(db), + DynamicClassAnchor::ScopeOffset { scope, .. } => *scope, + } + } + + /// Returns the explicit base classes of this dynamic class. + /// + /// For assigned `type()` calls, bases are computed lazily using deferred inference + /// to handle forward references (e.g., `X = type("X", (tuple["X | None"],), {})`). + /// + /// For dangling `type()` calls, bases are computed eagerly at creation time and + /// stored directly on the anchor, since dangling calls cannot recursively reference + /// the class being defined. + /// + /// Returns an empty slice if the bases cannot be computed (e.g., due to a cycle) + /// or if the bases argument is not a tuple. + /// + /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). + pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { + /// Inner cached function for deferred inference of bases. + /// Only called for assigned `type()` calls where inference was deferred. + #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] + fn deferred_explicit_bases<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + ) -> Box<[Type<'db>]> { + let module = parsed_module(db, definition.file(db)).load(db); + + let value = definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments"); + let call_expr = value + .as_call_expr() + .expect("Definition value should be a call expression"); + + // The `bases` argument is the second positional argument. + let Some(bases_arg) = call_expr.arguments.args.get(1) else { + return Box::default(); + }; + + // Use `definition_expression_type` for deferred inference support. + let bases_type = definition_expression_type(db, definition, bases_arg); + + // For variable-length tuples (like `tuple[type, ...]`), we can't statically + // determine the bases, so return Unknown. + bases_type + .fixed_tuple_elements(db) + .map(Cow::into_owned) + .map(Into::into) + .unwrap_or_else(|| Box::from([Type::unknown()])) + } + + match self.anchor(db) { + // For dangling calls, bases are stored directly on the anchor. + DynamicClassAnchor::ScopeOffset { explicit_bases, .. } => explicit_bases.as_ref(), + // For assigned calls, use deferred inference. + DynamicClassAnchor::Definition(definition) => deferred_explicit_bases(db, *definition), + } + } + + /// Returns a [`Span`] with the range of the `type()` call expression. + /// + /// See [`Self::header_range`] for more details. + pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) + } + + /// Returns the range of the `type()` call expression that created this class. + pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + let scope = self.scope(db); + let file = scope.file(db); + let module = parsed_module(db, file).load(db); + + match self.anchor(db) { + DynamicClassAnchor::Definition(definition) => { + // For definitions, get the range from the definition's value. + // The `type()` call is the value of the assignment. + definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments") + .range() + } + DynamicClassAnchor::ScopeOffset { offset, .. } => { + // For dangling `type()` calls, compute the absolute index from the offset. + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("anchor should not be NodeIndex::NONE"); + let absolute_index = NodeIndex::from(anchor_u32 + *offset); + + // Get the node and return its range. + let node: &ast::ExprCall = module + .get_by_index(absolute_index) + .try_into() + .expect("scope offset should point to ExprCall"); + node.range() + } + } + } + + /// Get the metaclass of this dynamic class. + /// + /// Derives the metaclass from base classes: finds the most derived metaclass + /// that is a subclass of all other base metaclasses. + /// + /// See + pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + self.try_metaclass(db) + .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) + } + + /// Try to get the metaclass of this dynamic class. + /// + /// Returns `Err(DynamicMetaclassConflict)` if there's a metaclass conflict + /// (i.e., two base classes have metaclasses that are not in a subclass relationship). + /// + /// See + pub(crate) fn try_metaclass( + self, + db: &'db dyn Db, + ) -> Result, DynamicMetaclassConflict<'db>> { + let original_bases = self.explicit_bases(db); + + // If no bases, metaclass is `type`. + // To dynamically create a class with no bases that has a custom metaclass, + // you have to invoke that metaclass rather than `type()`. + if original_bases.is_empty() { + return Ok(KnownClass::Type.to_class_literal(db)); + } + + // If there's an MRO error, return unknown to avoid cascading errors. + if self.try_mro(db).is_err() { + return Ok(SubclassOfType::subclass_of_unknown()); + } + + // Convert Types to ClassBases for metaclass computation. + // All bases should convert successfully here: `try_mro()` above would have + // returned `Err(InvalidBases)` if any failed, causing us to return early. + let bases: Vec> = original_bases + .iter() + .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, None)) + .collect(); + + // If all bases failed to convert, return type as the metaclass. + if bases.is_empty() { + return Ok(KnownClass::Type.to_class_literal(db)); + } + + // Start with the first base's metaclass as the candidate. + let mut candidate = bases[0].metaclass(db); + + // Track which base the candidate metaclass came from. + let (mut candidate_base, rest) = bases.split_first().unwrap(); + + // Reconcile with other bases' metaclasses. + for base in rest { + let base_metaclass = base.metaclass(db); + + // Get the ClassType for comparison. + let Some(candidate_class) = candidate.to_class_type(db) else { + // If candidate isn't a class type, keep it as is. + continue; + }; + let Some(base_metaclass_class) = base_metaclass.to_class_type(db) else { + continue; + }; + + // If base's metaclass is more derived, use it. + if base_metaclass_class.is_subclass_of(db, candidate_class) { + candidate = base_metaclass; + candidate_base = base; + continue; + } + + // If candidate is already more derived, keep it. + if candidate_class.is_subclass_of(db, base_metaclass_class) { + continue; + } + + // Conflict: neither metaclass is a subclass of the other. + // Python raises `TypeError: metaclass conflict` at runtime. + return Err(DynamicMetaclassConflict { + metaclass1: candidate_class, + base1: *candidate_base, + metaclass2: base_metaclass_class, + base2: *base, + }); + } + + Ok(candidate) + } + + /// Iterate over the MRO of this class using C3 linearization. + /// + /// The MRO includes the class itself as the first element, followed + /// by the merged base class MROs (consistent with `ClassType::iter_mro`). + /// + /// If the MRO cannot be computed (e.g., due to inconsistent ordering), falls back + /// to iterating over base MROs sequentially with deduplication. + pub(crate) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { + MroIterator::new(db, ClassLiteral::Dynamic(self), None) + } + + /// Look up an instance member by iterating through the MRO. + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + match MroLookup::new(db, self.iter_mro(db)).instance_member(name) { + InstanceMemberResult::Done(result) => result, + InstanceMemberResult::TypedDict => { + // Simplified `TypedDict` handling without type mapping. + KnownClass::TypedDictFallback + .to_instance(db) + .instance_member(db, name) + } + } + } + + /// Look up a class-level member by iterating through the MRO. + /// + /// Uses `MroLookup` with: + /// - No inherited generic context (dynamic classes aren't generic). + /// - `is_self_object = false` (dynamic classes are never `object`). + pub(crate) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + // Check if this dynamic class is dataclass-like (via dataclass_transform inheritance). + if matches!( + CodeGeneratorKind::from_class(db, self.into(), None), + Some(CodeGeneratorKind::DataclassLike(_)) + ) { + if name == "__dataclass_fields__" { + // Make this class look like a subclass of the `DataClassInstance` protocol. + return Place::declared(KnownClass::Dict.to_specialized_instance( + db, + &[ + KnownClass::Str.to_instance(db), + KnownClass::Field.to_specialized_instance(db, &[Type::any()]), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR); + } else if name == "__dataclass_params__" { + // There is no typeshed class for this. For now, we model it as `Any`. + return Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR); + } + } + + let result = MroLookup::new(db, self.iter_mro(db)).class_member( + name, policy, None, // No inherited generic context. + false, // Dynamic classes are never `object`. + ); + + match result { + ClassMemberResult::Done(result) => result.finalize(db), + ClassMemberResult::TypedDict => { + // Simplified `TypedDict` handling without type mapping. + KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("Will return Some() when called on class literal") + } + } + } + + /// Look up a class member defined directly on this class (not inherited). + /// + /// Returns [`Member::unbound`] if the member is not found in the namespace dict, + /// unless the namespace is dynamic, in which case returns `Unknown`. + pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + // If the namespace is dynamic (not a literal dict) and the name isn't in `self.members`, + // return Unknown since we can't know what attributes might be defined. + self.members(db) + .iter() + .find_map(|(member_name, ty)| (name == member_name).then_some(*ty)) + .or_else(|| self.has_dynamic_namespace(db).then(Type::unknown)) + .map(Member::definitely_declared) + .unwrap_or_default() + } + + /// Look up an instance member defined directly on this class (not inherited). + /// + /// For dynamic classes, instance members are the same as class members + /// since they come from the namespace dict. + pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + self.own_class_member(db, name) + } + + /// Try to compute the MRO for this dynamic class. + /// + /// Returns `Ok(Mro)` if successful, or `Err(DynamicMroError)` if there's + /// an error (duplicate bases or C3 linearization failure). + #[salsa::tracked(returns(ref), cycle_initial=dynamic_class_try_mro_cycle_initial, heap_size = ruff_memory_usage::heap_size)] + pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> { + Mro::of_dynamic_class(db, self) + } + + /// Return `Some()` if this dynamic class is known to be a [`DisjointBase`]. + /// + /// A dynamic class is a disjoint base if `__slots__` is defined in the namespace + /// dictionary and is non-empty. Example: + /// ```python + /// X = type("X", (), {"__slots__": ("a",)}) + /// ``` + pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + // Check if __slots__ is in the members + for (name, ty) in self.members(db) { + if name.as_str() == "__slots__" { + // Check if the slots are non-empty + let is_non_empty = match ty { + // __slots__ = ("a", "b") + Type::NominalInstance(nominal) => nominal.tuple_spec(db).is_some_and(|spec| { + spec.len().into_fixed_length().is_some_and(|len| len > 0) + }), + // __slots__ = "abc" # Same as ("abc",) + Type::LiteralValue(literal) if literal.is_string() => true, + // Other types are considered dynamic/unknown + _ => false, + }; + if is_non_empty { + return Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Dynamic( + self, + ))); + } + } + } + None + } + + /// Returns `true` if this dynamic class defines any ordering method (`__lt__`, `__le__`, + /// `__gt__`, `__ge__`) in its namespace dictionary. Used by `@total_ordering` to determine + /// if synthesis is valid. + /// + /// If the namespace is dynamic, returns `true` since we can't know if ordering methods exist. + pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + const ORDERING_METHODS: &[&str] = &["__lt__", "__le__", "__gt__", "__ge__"]; + ORDERING_METHODS + .iter() + .any(|name| !self.own_class_member(db, name).is_undefined()) + } + + /// Returns a new [`DynamicClassLiteral`] with the given dataclass params, preserving all other fields. + pub(crate) fn with_dataclass_params( + self, + db: &'db dyn Db, + dataclass_params: Option>, + ) -> Self { + Self::new( + db, + self.name(db).clone(), + self.anchor(db).clone(), + self.members(db), + self.has_dynamic_namespace(db), + dataclass_params, + ) + } +} + +/// Error for metaclass conflicts in dynamic classes. +/// +/// This mirrors `MetaclassErrorKind::Conflict` for regular classes. +#[derive(Debug, Clone)] +pub(crate) struct DynamicMetaclassConflict<'db> { + /// The first conflicting metaclass and its originating base class. + pub(crate) metaclass1: ClassType<'db>, + pub(crate) base1: ClassBase<'db>, + /// The second conflicting metaclass and its originating base class. + pub(crate) metaclass2: ClassType<'db>, + pub(crate) base2: ClassBase<'db>, +} + +#[expect(clippy::unnecessary_wraps)] +fn dynamic_class_try_mro_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: DynamicClassLiteral<'db>, +) -> Result, DynamicMroError<'db>> { + // When there's a cycle, return a minimal MRO with just the class itself and object. + // This breaks the cycle and allows type checking to continue. + Ok(Mro::from([ + ClassBase::Class(ClassType::NonGeneric(self_.into())), + ClassBase::object(db), + ])) +} diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs new file mode 100644 index 0000000000000..cc85970a0132a --- /dev/null +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -0,0 +1,1912 @@ +use crate::{ + Db, Program, + place::{DefinedPlace, Definedness, Place, known_module_symbol}, + semantic_index::{SemanticIndex, scope::NodeWithScopeKind}, + types::{ + Binding, ClassLiteral, ClassType, GenericContext, KnownInstanceType, StaticClassLiteral, + SubclassOfType, Truthiness, Type, binding_type, + bound_super::{BoundSuperError, BoundSuperType}, + class::CodeGeneratorKind, + constraints::{ConstraintSet, ConstraintSetBuilder}, + context::InferContext, + diagnostic::SUPER_CALL_IN_NAMED_TUPLE_METHOD, + infer::nearest_enclosing_class, + known_instance::DeprecatedInstance, + }, +}; +use ruff_db::files::File; +use ruff_python_ast as ast; +use ruff_python_ast::PythonVersion; +use rustc_hash::FxHashSet; +use std::{ + borrow::Cow, + sync::{LazyLock, Mutex}, +}; +use ty_module_resolver::{KnownModule, file_to_module}; + +/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow +/// for easier syntax when interacting with very common classes. +/// +/// Feel free to expand this enum if you ever find yourself using the same class in multiple +/// places. +/// Note: good candidates are any classes in `[ty_module_resolver::module::KnownModule]` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +#[cfg_attr(test, derive(strum_macros::EnumIter))] +pub enum KnownClass { + // To figure out where an stdlib symbol is defined, you can go into `crates/ty_vendored` + // and grep for the symbol name in any `.pyi` file. + + // Builtins + Bool, + Object, + Bytes, + Bytearray, + Type, + Int, + Float, + Complex, + Str, + List, + Tuple, + Set, + FrozenSet, + Dict, + Slice, + Property, + BaseException, + Exception, + BaseExceptionGroup, + ExceptionGroup, + Staticmethod, + Classmethod, + Super, + NotImplementedError, + // enum + Enum, + EnumType, + Auto, + Member, + Nonmember, + StrEnum, + // abc + ABCMeta, + // Types + GenericAlias, + ModuleType, + FunctionType, + MethodType, + MethodWrapperType, + WrapperDescriptorType, + UnionType, + GeneratorType, + AsyncGeneratorType, + CoroutineType, + NotImplementedType, + BuiltinFunctionType, + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + EllipsisType, + // Typeshed + NoneType, // Part of `types` for Python >= 3.10 + // Typing + Awaitable, + Generator, + Deprecated, + StdlibAlias, + SpecialForm, + TypeVar, + ParamSpec, + // typing_extensions.ParamSpec + ExtensionsParamSpec, // must be distinct from typing.ParamSpec, backports new features + ParamSpecArgs, + ParamSpecKwargs, + ProtocolMeta, + TypeVarTuple, + TypeAliasType, + NoDefaultType, + NewType, + SupportsIndex, + Iterable, + Iterator, + Sequence, + Mapping, + // typing_extensions + ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features + // Collections + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, + // sys + VersionInfo, + // dataclasses + Field, + KwOnly, + InitVar, + // _typeshed._type_checker_internals + NamedTupleFallback, + NamedTupleLike, + TypedDictFallback, + // string.templatelib + Template, + // pathlib + Path, + // ty_extensions + ConstraintSet, + GenericContext, + Specialization, +} + +impl KnownClass { + pub(crate) const fn is_bool(self) -> bool { + matches!(self, Self::Bool) + } + + pub(crate) const fn is_special_form(self) -> bool { + matches!(self, Self::SpecialForm) + } + + /// Determine whether instances of this class are always truthy, always falsy, + /// or have an ambiguous truthiness. + /// + /// Returns `None` for `KnownClass::Tuple`, since the truthiness of a tuple + /// depends on its spec. + pub(crate) const fn bool(self) -> Option { + match self { + // N.B. It's only generally safe to infer `Truthiness::AlwaysTrue` for a `KnownClass` + // variant if the class's `__bool__` method always returns the same thing *and* the + // class is `@final`. + // + // E.g. `ModuleType.__bool__` always returns `True`, but `ModuleType` is not `@final`. + // Equally, `range` is `@final`, but its `__bool__` method can return `False`. + Self::EllipsisType + | Self::NoDefaultType + | Self::MethodType + | Self::Slice + | Self::FunctionType + | Self::VersionInfo + | Self::TypeAliasType + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Super + | Self::WrapperDescriptorType + | Self::UnionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::MethodWrapperType + | Self::CoroutineType + | Self::BuiltinFunctionType + | Self::Template + | Self::Path => Some(Truthiness::AlwaysTrue), + + Self::NoneType => Some(Truthiness::AlwaysFalse), + + Self::BaseException + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Object + | Self::OrderedDict + | Self::BaseExceptionGroup + | Self::Bool + | Self::Str + | Self::List + | Self::GenericAlias + | Self::NewType + | Self::StdlibAlias + | Self::SupportsIndex + | Self::Set + | Self::Int + | Self::Type + | Self::Bytes + | Self::Bytearray + | Self::FrozenSet + | Self::Property + | Self::SpecialForm + | Self::Dict + | Self::ModuleType + | Self::ChainMap + | Self::Complex + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::Float + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 + // and raises a `TypeError` in Python >=3.14 + // (see https://docs.python.org/3/library/constants.html#NotImplemented) + | Self::NotImplementedType + | Self::Staticmethod + | Self::Classmethod + | Self::Awaitable + | Self::Generator + | Self::Deprecated + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::NamedTupleFallback + | Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::ProtocolMeta + | Self::TypedDictFallback => Some(Truthiness::Ambiguous), + + Self::Tuple => None, + } + } + + /// Return `true` if this class is a subclass of `enum.Enum` *and* has enum members, i.e. + /// if it is an "actual" enum, not `enum.Enum` itself or a similar custom enum class. + pub(crate) const fn is_enum_subclass_with_members(self) -> bool { + match self { + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Tuple + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::NotImplementedError + | KnownClass::Exception + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::StrEnum + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NoneType + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar + | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::Sequence + | KnownClass::Mapping + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::VersionInfo + | KnownClass::EllipsisType + | KnownClass::NotImplementedType + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::InitVar + | KnownClass::NamedTupleFallback + | KnownClass::NamedTupleLike + | KnownClass::ConstraintSet + | KnownClass::GenericContext + | KnownClass::Specialization + | KnownClass::TypedDictFallback + | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta + | KnownClass::Template + | KnownClass::Path => false, + } + } + + /// Return `true` if this class is a (true) subclass of `typing.TypedDict`. + pub(crate) const fn is_typed_dict_subclass(self) -> bool { + match self { + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Tuple + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::Exception + | KnownClass::NotImplementedError + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::StrEnum + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NoneType + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar + | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::Sequence + | KnownClass::Mapping + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::VersionInfo + | KnownClass::EllipsisType + | KnownClass::NotImplementedType + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::InitVar + | KnownClass::NamedTupleFallback + | KnownClass::NamedTupleLike + | KnownClass::ConstraintSet + | KnownClass::GenericContext + | KnownClass::Specialization + | KnownClass::TypedDictFallback + | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta + | KnownClass::Template + | KnownClass::Path => false, + } + } + + pub(crate) const fn is_tuple_subclass(self) -> bool { + match self { + KnownClass::Tuple | KnownClass::VersionInfo => true, + + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::Exception + | KnownClass::NotImplementedError + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::StrEnum + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NoneType + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar + | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::Sequence + | KnownClass::Mapping + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::EllipsisType + | KnownClass::NotImplementedType + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::InitVar + | KnownClass::TypedDictFallback + | KnownClass::NamedTupleLike + | KnownClass::NamedTupleFallback + | KnownClass::ConstraintSet + | KnownClass::GenericContext + | KnownClass::Specialization + | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta + | KnownClass::Template + | KnownClass::Path => false, + } + } + + /// Return `true` if this class is a protocol class. + /// + /// In an ideal world, perhaps we wouldn't hardcode this knowledge here; + /// instead, we'd just look at the bases for these classes, as we do for + /// all other classes. However, the special casing here helps us out in + /// two important ways: + /// + /// 1. It helps us avoid Salsa cycles when creating types such as "instance of `str`" + /// and "instance of `sys._version_info`". These types are constructed very early + /// on, but it causes problems if we attempt to infer the types of their bases + /// too soon. + /// 2. It's probably more performant. + pub(crate) const fn is_protocol(self) -> bool { + match self { + Self::SupportsIndex + | Self::Iterable + | Self::Iterator + | Self::Awaitable + | Self::NamedTupleLike + | Self::Generator => true, + + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Tuple + | Self::Int + | Self::Float + | Self::Complex + | Self::FrozenSet + | Self::Str + | Self::Set + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::Property + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Deprecated + | Self::GenericAlias + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::NoneType + | Self::SpecialForm + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::TypeAliasType + | Self::NoDefaultType + | Self::NewType + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Super + | Self::StdlibAlias + | Self::VersionInfo + | Self::EllipsisType + | Self::NotImplementedType + | Self::UnionType + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::NamedTupleFallback + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::TypedDictFallback + | Self::BuiltinFunctionType + | Self::ProtocolMeta + | Self::Template + | Self::Path + | Self::Mapping + | Self::Sequence => false, + } + } + + /// Return `true` if this class is a typeshed fallback class which is used to provide attributes and + /// methods for another type (e.g. `NamedTupleFallback` for actual `NamedTuple`s). These fallback + /// classes need special treatment in some places. For example, implicit usages of `Self` should not + /// be eagerly replaced with the fallback class itself. Instead, `Self` should eventually be treated + /// as referring to the destination type (e.g. the actual `NamedTuple`). + pub(crate) const fn is_fallback_class(self) -> bool { + match self { + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Tuple + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::Exception + | KnownClass::NotImplementedError + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::StrEnum + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NotImplementedType + | KnownClass::BuiltinFunctionType + | KnownClass::EllipsisType + | KnownClass::NoneType + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar + | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::ProtocolMeta + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::Sequence + | KnownClass::Mapping + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::VersionInfo + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::NamedTupleLike + | KnownClass::Template + | KnownClass::Path + | KnownClass::ConstraintSet + | KnownClass::GenericContext + | KnownClass::Specialization + | KnownClass::InitVar => false, + KnownClass::NamedTupleFallback | KnownClass::TypedDictFallback => true, + } + } + + pub(crate) fn name(self, db: &dyn Db) -> &'static str { + match self { + Self::Bool => "bool", + Self::Object => "object", + Self::Bytes => "bytes", + Self::Bytearray => "bytearray", + Self::Tuple => "tuple", + Self::Int => "int", + Self::Float => "float", + Self::Complex => "complex", + Self::FrozenSet => "frozenset", + Self::Str => "str", + Self::Set => "set", + Self::Dict => "dict", + Self::List => "list", + Self::Type => "type", + Self::Slice => "slice", + Self::Property => "property", + Self::BaseException => "BaseException", + Self::BaseExceptionGroup => "BaseExceptionGroup", + Self::Exception => "Exception", + Self::NotImplementedError => "NotImplementedError", + Self::ExceptionGroup => "ExceptionGroup", + Self::Staticmethod => "staticmethod", + Self::Classmethod => "classmethod", + Self::Awaitable => "Awaitable", + Self::Generator => "Generator", + Self::Deprecated => "deprecated", + Self::GenericAlias => "GenericAlias", + Self::ModuleType => "ModuleType", + Self::FunctionType => "FunctionType", + Self::MethodType => "MethodType", + Self::UnionType => "UnionType", + Self::MethodWrapperType => "MethodWrapperType", + Self::WrapperDescriptorType => "WrapperDescriptorType", + Self::BuiltinFunctionType => "BuiltinFunctionType", + Self::GeneratorType => "GeneratorType", + Self::AsyncGeneratorType => "AsyncGeneratorType", + Self::CoroutineType => "CoroutineType", + Self::NoneType => "NoneType", + Self::SpecialForm => "_SpecialForm", + Self::TypeVar => "TypeVar", + Self::ExtensionsTypeVar => "TypeVar", + Self::ParamSpec => "ParamSpec", + Self::ExtensionsParamSpec => "ParamSpec", + Self::ParamSpecArgs => "ParamSpecArgs", + Self::ParamSpecKwargs => "ParamSpecKwargs", + Self::TypeVarTuple => "TypeVarTuple", + Self::TypeAliasType => "TypeAliasType", + Self::NoDefaultType => "_NoDefaultType", + Self::NewType => "NewType", + Self::SupportsIndex => "SupportsIndex", + Self::ChainMap => "ChainMap", + Self::Counter => "Counter", + Self::DefaultDict => "defaultdict", + Self::Deque => "deque", + Self::OrderedDict => "OrderedDict", + Self::Enum => "Enum", + Self::EnumType => { + if Program::get(db).python_version(db) >= PythonVersion::PY311 { + "EnumType" + } else { + "EnumMeta" + } + } + Self::Auto => "auto", + Self::Member => "member", + Self::Nonmember => "nonmember", + Self::StrEnum => "StrEnum", + Self::ABCMeta => "ABCMeta", + Self::Super => "super", + Self::Iterable => "Iterable", + Self::Iterator => "Iterator", + Self::Sequence => "Sequence", + Self::Mapping => "Mapping", + // For example, `typing.List` is defined as `List = _Alias()` in typeshed + Self::StdlibAlias => "_Alias", + // This is the name the type of `sys.version_info` has in typeshed, + // which is different to what `type(sys.version_info).__name__` is at runtime. + // (At runtime, `type(sys.version_info).__name__ == "version_info"`, + // which is impossible to replicate in the stubs since the sole instance of the class + // also has that name in the `sys` module.) + Self::VersionInfo => "_version_info", + Self::EllipsisType => { + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + "EllipsisType" + } else { + "ellipsis" + } + } + Self::NotImplementedType => { + // Exposed as `types.NotImplementedType` on Python >=3.10; + // backported as `builtins._NotImplementedType` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + "NotImplementedType" + } else { + "_NotImplementedType" + } + } + Self::Field => "Field", + Self::KwOnly => "KW_ONLY", + Self::InitVar => "InitVar", + Self::NamedTupleFallback => "NamedTupleFallback", + Self::NamedTupleLike => "NamedTupleLike", + Self::ConstraintSet => "ConstraintSet", + Self::GenericContext => "GenericContext", + Self::Specialization => "Specialization", + Self::TypedDictFallback => "TypedDictFallback", + Self::Template => "Template", + Self::Path => "Path", + Self::ProtocolMeta => "_ProtocolMeta", + } + } + + pub(crate) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { + struct KnownClassDisplay<'db> { + db: &'db dyn Db, + class: KnownClass, + } + + impl std::fmt::Display for KnownClassDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let KnownClassDisplay { + class: known_class, + db, + } = *self; + write!( + f, + "{module}.{class}", + module = known_class.canonical_module(db), + class = known_class.name(db) + ) + } + } + + KnownClassDisplay { db, class: self } + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing all possible instances of + /// the class. If this class is generic, this will use the default specialization. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + #[track_caller] + pub fn to_instance(self, db: &dyn Db) -> Type<'_> { + debug_assert_ne!( + self, + KnownClass::Tuple, + "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" + ); + self.to_class_literal(db) + .to_class_type(db) + .map(|class| Type::instance(db, class)) + .unwrap_or_else(Type::unknown) + } + + /// Similar to [`KnownClass::to_instance`], but returns the Unknown-specialization where each type + /// parameter is specialized to `Unknown`. + #[track_caller] + pub(crate) fn to_instance_unknown(self, db: &dyn Db) -> Type<'_> { + debug_assert_ne!( + self, + KnownClass::Tuple, + "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" + ); + self.try_to_class_literal(db) + .map(|literal| Type::instance(db, literal.unknown_specialization(db))) + .unwrap_or_else(Type::unknown) + } + + /// Lookup a generic [`KnownClass`] in typeshed and return a [`Type`] + /// representing a specialization of that class. + /// + /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong + /// number of types, a debug-level log message will be emitted stating this. + pub(crate) fn to_specialized_class_type<'t, 'db, T>( + self, + db: &'db dyn Db, + specialization: T, + ) -> Option> + where + T: Into]>>, + 'db: 't, + { + fn to_specialized_class_type_impl<'db>( + db: &'db dyn Db, + class: KnownClass, + class_literal: StaticClassLiteral<'db>, + specialization: Cow<[Type<'db>]>, + generic_context: GenericContext<'db>, + ) -> ClassType<'db> { + if specialization.len() != generic_context.len(db) { + // a cache of the `KnownClass`es that we have already seen mismatched-arity + // specializations for (and therefore that we've already logged a warning for) + static MESSAGES: LazyLock>> = + LazyLock::new(Mutex::default); + if MESSAGES.lock().unwrap().insert(class) { + tracing::info!( + "Wrong number of types when specializing {}. \ + Falling back to default specialization for the symbol instead.", + class.display(db) + ); + } + return class_literal.default_specialization(db); + } + + class_literal + .apply_specialization(db, |_| generic_context.specialize(db, specialization)) + } + + let class_literal = self.to_class_literal(db).as_class_literal()?.as_static()?; + let generic_context = class_literal.generic_context(db)?; + let specialization = specialization.into(); + + Some(to_specialized_class_type_impl( + db, + self, + class_literal, + specialization, + generic_context, + )) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing all possible instances of the generic class with a specialization. + /// + /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong + /// number of types, a debug-level log message will be emitted stating this. + #[track_caller] + pub(crate) fn to_specialized_instance<'t, 'db, T>( + self, + db: &'db dyn Db, + specialization: T, + ) -> Type<'db> + where + T: Into]>>, + 'db: 't, + { + debug_assert_ne!( + self, + KnownClass::Tuple, + "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" + ); + self.to_specialized_class_type(db, specialization) + .and_then(|class_type| Type::from(class_type).to_instance(db)) + .unwrap_or_else(Type::unknown) + } + + /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// Return an error if the symbol cannot be found in the expected typeshed module, + /// or if the symbol is not a class definition, or if the symbol is possibly unbound. + fn try_to_class_literal_without_logging( + self, + db: &dyn Db, + ) -> Result, KnownClassLookupError<'_>> { + let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place; + match symbol { + Place::Defined(DefinedPlace { + ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), + definedness: Definedness::AlwaysDefined, + .. + }) => Ok(class_literal), + Place::Defined(DefinedPlace { + ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), + definedness: Definedness::PossiblyUndefined, + .. + }) => Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }), + Place::Defined(DefinedPlace { ty: found_type, .. }) => { + Err(KnownClassLookupError::SymbolNotAClass { found_type }) + } + Place::Undefined => Err(KnownClassLookupError::ClassNotFound), + } + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> { + #[salsa::interned(heap_size=ruff_memory_usage::heap_size)] + struct KnownClassArgument { + class: KnownClass, + } + + fn known_class_to_class_literal_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _class: KnownClassArgument<'db>, + ) -> Option> { + None + } + + #[salsa::tracked(cycle_initial=known_class_to_class_literal_initial, heap_size=ruff_memory_usage::heap_size)] + fn known_class_to_class_literal<'db>( + db: &'db dyn Db, + class: KnownClassArgument<'db>, + ) -> Option> { + let class = class.class(db); + class + .try_to_class_literal_without_logging(db) + .or_else(|lookup_error| { + if matches!( + lookup_error, + KnownClassLookupError::ClassPossiblyUnbound { .. } + ) { + tracing::info!("{}", lookup_error.display(db, class)); + } else { + tracing::info!( + "{}. Falling back to `Unknown` for the symbol instead.", + lookup_error.display(db, class) + ); + } + + match lookup_error { + KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { + Ok(class_literal) + } + KnownClassLookupError::ClassNotFound { .. } + | KnownClassLookupError::SymbolNotAClass { .. } => Err(()), + } + }) + .ok() + } + + known_class_to_class_literal(db, KnownClassArgument::new(db, self)) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type<'_> { + self.try_to_class_literal(db) + .map(|class| Type::ClassLiteral(ClassLiteral::Static(class))) + .unwrap_or_else(Type::unknown) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing that class and all possible subclasses of the class. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub fn to_subclass_of(self, db: &dyn Db) -> Type<'_> { + self.to_class_literal(db) + .to_class_type(db) + .map(|class| SubclassOfType::from(db, class)) + .unwrap_or_else(SubclassOfType::subclass_of_unknown) + } + + /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, + /// *and* `class` is a subclass of `other`. + pub(crate) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + self.try_to_class_literal_without_logging(db) + .is_ok_and(|class| class.is_subclass_of(db, None, other)) + } + + pub(crate) fn when_subclass_of<'db, 'c>( + self, + db: &'db dyn Db, + other: ClassType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, + ) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(constraints, self.is_subclass_of(db, other)) + } + + /// Return the module in which we should look up the definition for this class + pub(super) fn canonical_module(self, db: &dyn Db) -> KnownModule { + match self { + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Tuple + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Slice + | Self::Super + | Self::Property => KnownModule::Builtins, + Self::VersionInfo => KnownModule::Sys, + Self::ABCMeta => KnownModule::Abc, + Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum => KnownModule::Enum, + Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::MethodWrapperType + | Self::UnionType + | Self::BuiltinFunctionType + | Self::WrapperDescriptorType => KnownModule::Types, + Self::NoneType => KnownModule::Typeshed, + Self::Awaitable + | Self::Generator + | Self::SpecialForm + | Self::TypeVar + | Self::StdlibAlias + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + | Self::ProtocolMeta + | Self::SupportsIndex => KnownModule::Typing, + Self::TypeAliasType + | Self::ExtensionsTypeVar + | Self::TypeVarTuple + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::Deprecated + | Self::NewType => KnownModule::TypingExtensions, + Self::ParamSpec => { + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Typing + } else { + KnownModule::TypingExtensions + } + } + Self::NoDefaultType => { + let python_version = Program::get(db).python_version(db); + + // typing_extensions has a 3.13+ re-export for the `typing.NoDefault` + // singleton, but not for `typing._NoDefaultType`. So we need to switch + // to `typing._NoDefaultType` for newer versions: + if python_version >= PythonVersion::PY313 { + KnownModule::Typing + } else { + KnownModule::TypingExtensions + } + } + Self::EllipsisType => { + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Types + } else { + KnownModule::Builtins + } + } + Self::NotImplementedType => { + // Exposed as `types.NotImplementedType` on Python >=3.10; + // backported as `builtins._NotImplementedType` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Types + } else { + KnownModule::Builtins + } + } + Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict => KnownModule::Collections, + Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses, + Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals, + Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization => KnownModule::TyExtensions, + Self::Template => KnownModule::Templatelib, + Self::Path => KnownModule::Pathlib, + } + } + + /// Returns `Some(true)` if all instances of this `KnownClass` compare equal. + /// Returns `None` for `KnownClass::Tuple`, since whether or not a tuple type + /// is single-valued depends on the tuple spec. + pub(crate) const fn is_single_valued(self) -> Option { + match self { + Self::NoneType + | Self::NoDefaultType + | Self::VersionInfo + | Self::EllipsisType + | Self::TypeAliasType + | Self::UnionType + | Self::NotImplementedType => Some(true), + + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::Slice + | Self::Property + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Awaitable + | Self::Generator + | Self::Deprecated + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::SpecialForm + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::SupportsIndex + | Self::StdlibAlias + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Super + | Self::NewType + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + | Self::NamedTupleFallback + | Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::TypedDictFallback + | Self::BuiltinFunctionType + | Self::ProtocolMeta + | Self::Template + | Self::Path => Some(false), + + Self::Tuple => None, + } + } + + /// Is this class a singleton class? + /// + /// A singleton class is a class where it is known that only one instance can ever exist at runtime. + pub(crate) const fn is_singleton(self) -> bool { + match self { + Self::NoneType + | Self::EllipsisType + | Self::NoDefaultType + | Self::VersionInfo + | Self::TypeAliasType + | Self::NotImplementedType => true, + + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Tuple + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::Property + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::SpecialForm + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::StdlibAlias + | Self::SupportsIndex + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Awaitable + | Self::Generator + | Self::Deprecated + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Super + | Self::UnionType + | Self::NewType + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + | Self::NamedTupleFallback + | Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::TypedDictFallback + | Self::BuiltinFunctionType + | Self::ProtocolMeta + | Self::Template + | Self::Path => false, + } + } + + pub(crate) fn try_from_file_and_name( + db: &dyn Db, + file: File, + class_name: &str, + ) -> Option { + // We assert that this match is exhaustive over the right-hand side in the unit test + // `known_class_roundtrip_from_str()` + let candidates: &[Self] = match class_name { + "bool" => &[Self::Bool], + "object" => &[Self::Object], + "bytes" => &[Self::Bytes], + "bytearray" => &[Self::Bytearray], + "tuple" => &[Self::Tuple], + "type" => &[Self::Type], + "int" => &[Self::Int], + "float" => &[Self::Float], + "complex" => &[Self::Complex], + "str" => &[Self::Str], + "set" => &[Self::Set], + "frozenset" => &[Self::FrozenSet], + "dict" => &[Self::Dict], + "list" => &[Self::List], + "slice" => &[Self::Slice], + "property" => &[Self::Property], + "BaseException" => &[Self::BaseException], + "BaseExceptionGroup" => &[Self::BaseExceptionGroup], + "Exception" => &[Self::Exception], + "NotImplementedError" => &[Self::NotImplementedError], + "ExceptionGroup" => &[Self::ExceptionGroup], + "staticmethod" => &[Self::Staticmethod], + "classmethod" => &[Self::Classmethod], + "Awaitable" => &[Self::Awaitable], + "Generator" => &[Self::Generator], + "deprecated" => &[Self::Deprecated], + "GenericAlias" => &[Self::GenericAlias], + "NoneType" => &[Self::NoneType], + "ModuleType" => &[Self::ModuleType], + "GeneratorType" => &[Self::GeneratorType], + "AsyncGeneratorType" => &[Self::AsyncGeneratorType], + "CoroutineType" => &[Self::CoroutineType], + "FunctionType" => &[Self::FunctionType], + "MethodType" => &[Self::MethodType], + "UnionType" => &[Self::UnionType], + "MethodWrapperType" => &[Self::MethodWrapperType], + "WrapperDescriptorType" => &[Self::WrapperDescriptorType], + "BuiltinFunctionType" => &[Self::BuiltinFunctionType], + "NewType" => &[Self::NewType], + "TypeAliasType" => &[Self::TypeAliasType], + "TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar], + "Iterable" => &[Self::Iterable], + "Iterator" => &[Self::Iterator], + "Sequence" => &[Self::Sequence], + "Mapping" => &[Self::Mapping], + "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec], + "ParamSpecArgs" => &[Self::ParamSpecArgs], + "ParamSpecKwargs" => &[Self::ParamSpecKwargs], + "TypeVarTuple" => &[Self::TypeVarTuple], + "ChainMap" => &[Self::ChainMap], + "Counter" => &[Self::Counter], + "defaultdict" => &[Self::DefaultDict], + "deque" => &[Self::Deque], + "OrderedDict" => &[Self::OrderedDict], + "_Alias" => &[Self::StdlibAlias], + "_SpecialForm" => &[Self::SpecialForm], + "_NoDefaultType" => &[Self::NoDefaultType], + "SupportsIndex" => &[Self::SupportsIndex], + "Enum" => &[Self::Enum], + "EnumMeta" => &[Self::EnumType], + "EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { + &[Self::EnumType] + } + "StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { + &[Self::StrEnum] + } + "auto" => &[Self::Auto], + "member" => &[Self::Member], + "nonmember" => &[Self::Nonmember], + "ABCMeta" => &[Self::ABCMeta], + "super" => &[Self::Super], + "_version_info" => &[Self::VersionInfo], + "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { + &[Self::EllipsisType] + } + "EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { + &[Self::EllipsisType] + } + "_NotImplementedType" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { + &[Self::NotImplementedType] + } + "NotImplementedType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { + &[Self::NotImplementedType] + } + "Field" => &[Self::Field], + "KW_ONLY" => &[Self::KwOnly], + "InitVar" => &[Self::InitVar], + "NamedTupleFallback" => &[Self::NamedTupleFallback], + "NamedTupleLike" => &[Self::NamedTupleLike], + "ConstraintSet" => &[Self::ConstraintSet], + "GenericContext" => &[Self::GenericContext], + "Specialization" => &[Self::Specialization], + "TypedDictFallback" => &[Self::TypedDictFallback], + "Template" => &[Self::Template], + "Path" => &[Self::Path], + "_ProtocolMeta" => &[Self::ProtocolMeta], + _ => return None, + }; + + let module = file_to_module(db, file)?.known(db)?; + + candidates + .iter() + .copied() + .find(|&candidate| candidate.check_module(db, module)) + } + + /// Return `true` if the module of `self` matches `module` + fn check_module(self, db: &dyn Db, module: KnownModule) -> bool { + match self { + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Tuple + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::Slice + | Self::Property + | Self::GenericAlias + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will + | Self::ModuleType + | Self::VersionInfo + | Self::BaseException + | Self::Exception + | Self::NotImplementedError + | Self::ExceptionGroup + | Self::EllipsisType + | Self::BaseExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum + | Self::ABCMeta + | Self::Super + | Self::NotImplementedType + | Self::UnionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::CoroutineType + | Self::WrapperDescriptorType + | Self::BuiltinFunctionType + | Self::Field + | Self::KwOnly + | Self::InitVar + | Self::NamedTupleFallback + | Self::TypedDictFallback + | Self::TypeVar + | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec + | Self::NamedTupleLike + | Self::ConstraintSet + | Self::GenericContext + | Self::Specialization + | Self::Awaitable + | Self::Generator + | Self::Template + | Self::Path => module == self.canonical_module(db), + Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), + Self::SpecialForm + | Self::TypeAliasType + | Self::NoDefaultType + | Self::SupportsIndex + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Iterable + | Self::Iterator + | Self::Sequence + | Self::Mapping + | Self::ProtocolMeta + | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), + Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), + } + } + + /// Evaluate a call to this known class, emit any diagnostics that are necessary + /// as a result of the call, and return the type that results from the call. + pub(crate) fn check_call<'db>( + self, + context: &InferContext<'db, '_>, + index: &SemanticIndex<'db>, + overload: &mut Binding<'db>, + call_expression: &ast::ExprCall, + ) { + let db = context.db(); + let scope = context.scope(); + let module = context.module(); + + match self { + KnownClass::Super => { + // Handle the case where `super()` is called with no arguments. + // In this case, we need to infer the two arguments: + // 1. The nearest enclosing class + // 2. The first parameter of the current function (typically `self` or `cls`) + match overload.parameter_types() { + [] => { + let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) + else { + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic(context, call_expression.into()); + overload.set_return_type(Type::unknown()); + return; + }; + + // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. + if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into(), None) { + if let Some(builder) = context + .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) + { + builder.into_diagnostic(format_args!( + "Cannot use `super()` in a method of NamedTuple class `{}`", + enclosing_class.name(db) + )); + } + overload.set_return_type(Type::unknown()); + return; + } + + // The type of the first parameter if the given scope is function-like (i.e. function or lambda). + // `None` if the scope is not function-like, or has no parameters. + let first_param = match scope.node(db) { + NodeWithScopeKind::Function(f) => { + f.node(module).parameters.iter().next() + } + NodeWithScopeKind::Lambda(l) => l + .node(module) + .parameters + .as_ref() + .into_iter() + .flatten() + .next(), + _ => None, + }; + + let Some(first_param) = first_param else { + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic(context, call_expression.into()); + overload.set_return_type(Type::unknown()); + return; + }; + + let definition = index.expect_single_definition(first_param); + let first_param = binding_type(db, definition); + + let bound_super = BoundSuperType::build( + db, + Type::ClassLiteral(ClassLiteral::Static(enclosing_class)), + first_param, + ) + .unwrap_or_else(|err| { + err.report_diagnostic(context, call_expression.into()); + Type::unknown() + }); + + overload.set_return_type(bound_super); + } + [Some(pivot_class_type), Some(owner_type)] => { + // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. + if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) { + if CodeGeneratorKind::NamedTuple.matches( + db, + enclosing_class.into(), + None, + ) { + if let Some(builder) = context + .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) + { + builder.into_diagnostic(format_args!( + "Cannot use `super()` in a method of NamedTuple class `{}`", + enclosing_class.name(db) + )); + } + overload.set_return_type(Type::unknown()); + return; + } + } + + let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type) + .unwrap_or_else(|err| { + err.report_diagnostic(context, call_expression.into()); + Type::unknown() + }); + overload.set_return_type(bound_super); + } + _ => {} + } + } + + KnownClass::Deprecated => { + // Parsing something of the form: + // + // @deprecated("message") + // @deprecated("message", category = DeprecationWarning, stacklevel = 1) + // + // "Static type checker behavior is not affected by the category and stacklevel arguments" + // so we only need the message and can ignore everything else. The message is mandatory, + // must be a LiteralString, and always comes first. + // + // We aren't guaranteed to know the static value of a LiteralString, so we need to + // accept that sometimes we will fail to include the message. + // + // We don't do any serious validation/diagnostics here, as the signature for this + // is included in `Type::bindings`. + // + // See: + let [Some(message), ..] = overload.parameter_types() else { + // Checking in Type::bindings will complain about this for us + return; + }; + + overload.set_return_type(Type::KnownInstance(KnownInstanceType::Deprecated( + DeprecatedInstance::new(db, message.as_string_literal()), + ))); + } + + _ => {} + } + } +} + +/// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum KnownClassLookupError<'db> { + /// There is no symbol by that name in the expected typeshed module. + ClassNotFound, + /// There is a symbol by that name in the expected typeshed module, + /// but it's not a class. + SymbolNotAClass { found_type: Type<'db> }, + /// There is a symbol by that name in the expected typeshed module, + /// and it's a class definition, but it's possibly unbound. + ClassPossiblyUnbound { + class_literal: StaticClassLiteral<'db>, + }, +} + +impl<'db> KnownClassLookupError<'db> { + fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db { + struct ErrorDisplay<'db> { + db: &'db dyn Db, + class: KnownClass, + error: KnownClassLookupError<'db>, + } + + impl std::fmt::Display for ErrorDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ErrorDisplay { db, class, error } = *self; + + let class = class.display(db); + let python_version = Program::get(db).python_version(db); + + match error { + KnownClassLookupError::ClassNotFound => write!( + f, + "Could not find class `{class}` in typeshed on Python {python_version}", + ), + KnownClassLookupError::SymbolNotAClass { found_type } => write!( + f, + "Error looking up `{class}` in typeshed: expected to find a class definition \ + on Python {python_version}, but found a symbol of type `{found_type}` instead", + found_type = found_type.display(db), + ), + KnownClassLookupError::ClassPossiblyUnbound { .. } => write!( + f, + "Error looking up `{class}` in typeshed on Python {python_version}: \ + expected to find a fully bound symbol, but found one that is possibly unbound", + ), + } + } + } + + ErrorDisplay { + db, + class, + error: *self, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::tests::setup_db; + use crate::{PythonVersionSource, PythonVersionWithSource}; + use salsa::Setter; + use strum::IntoEnumIterator; + use ty_module_resolver::resolve_module_confident; + + #[test] + fn known_class_roundtrip_from_str() { + let mut db = setup_db(); + Program::get(&db) + .set_python_version_with_source(&mut db) + .to(PythonVersionWithSource { + version: PythonVersion::latest_preview(), + source: PythonVersionSource::default(), + }); + for class in KnownClass::iter() { + let class_name = class.name(&db); + let class_module = + resolve_module_confident(&db, &class.canonical_module(&db).name()).unwrap(); + + assert_eq!( + KnownClass::try_from_file_and_name( + &db, + class_module.file(&db).unwrap(), + class_name + ), + Some(class), + "`KnownClass::candidate_from_str` appears to be missing a case for `{class_name}`" + ); + } + } + + #[test] + fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { + let mut db = setup_db(); + + Program::get(&db) + .set_python_version_with_source(&mut db) + .to(PythonVersionWithSource { + version: PythonVersion::latest_ty(), + source: PythonVersionSource::default(), + }); + + for class in KnownClass::iter() { + // Check the class can be looked up successfully + class.try_to_class_literal_without_logging(&db).unwrap(); + + // We can't call `KnownClass::Tuple.to_instance()`; + // there are assertions to ensure that we always call `Type::homogeneous_tuple()` + // or `Type::heterogeneous_tuple()` instead.` + if class != KnownClass::Tuple { + assert_ne!( + class.to_instance(&db), + Type::unknown(), + "Unexpectedly fell back to `Unknown` for `{class:?}`" + ); + } + } + } + + #[test] + fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { + let mut db = setup_db(); + + // First, collect the `KnownClass` variants + // and sort them according to the version they were added in. + // This makes the test far faster as it minimizes the number of times + // we need to change the Python version in the loop. + let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter() + .map(|class| { + let version_added = match class { + KnownClass::Template => PythonVersion::PY314, + KnownClass::UnionType => PythonVersion::PY310, + KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => { + PythonVersion::PY311 + } + KnownClass::GenericAlias => PythonVersion::PY39, + KnownClass::KwOnly => PythonVersion::PY310, + KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => { + PythonVersion::PY311 + } + KnownClass::ParamSpec => PythonVersion::PY310, + _ => PythonVersion::PY37, + }; + (class, version_added) + }) + .collect(); + + classes.sort_unstable_by_key(|(_, version)| *version); + + let program = Program::get(&db); + let mut current_version = program.python_version(&db); + + for (class, version_added) in classes { + if version_added != current_version { + program + .set_python_version_with_source(&mut db) + .to(PythonVersionWithSource { + version: version_added, + source: PythonVersionSource::default(), + }); + current_version = version_added; + } + + // Check the class can be looked up successfully + class.try_to_class_literal_without_logging(&db).unwrap(); + + // We can't call `KnownClass::Tuple.to_instance()`; + // there are assertions to ensure that we always call `Type::homogeneous_tuple()` + // or `Type::heterogeneous_tuple()` instead.` + if class != KnownClass::Tuple { + assert_ne!( + class.to_instance(&db), + Type::unknown(), + "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {version_added}" + ); + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/class/named_tuple.rs b/crates/ty_python_semantic/src/types/class/named_tuple.rs new file mode 100644 index 0000000000000..3c4b570f8e928 --- /dev/null +++ b/crates/ty_python_semantic/src/types/class/named_tuple.rs @@ -0,0 +1,582 @@ +use ruff_db::{diagnostic::Span, parsed::parsed_module}; +use ruff_python_ast as ast; +use ruff_python_ast::{NodeIndex, PythonVersion, name::Name}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::{ + Db, Program, + place::{Place, PlaceAndQualifiers}, + semantic_index::{definition::Definition, scope::ScopeId}, + types::{ + BindingContext, BoundTypeVarInstance, ClassBase, ClassLiteral, ClassType, GenericContext, + KnownClass, KnownInstanceType, MemberLookupPolicy, Parameter, Parameters, + PropertyInstanceType, Signature, SubclassOfType, Type, TypeContext, TypeMapping, + definition_expression_type, member::Member, mro::Mro, tuple::TupleType, + }, +}; + +/// Synthesize a namedtuple class member given the field information. +/// +/// This is used by both `DynamicNamedTupleLiteral` and `StaticClassLiteral` (for declarative +/// namedtuples) to avoid duplicating the synthesis logic. +/// +/// The `inherited_generic_context` parameter is used for declarative namedtuples to preserve +/// generic context in the synthesized `__new__` signature. +pub(super) fn synthesize_namedtuple_class_member<'db>( + db: &'db dyn Db, + name: &str, + instance_ty: Type<'db>, + fields: impl Iterator>, + inherited_generic_context: Option>, +) -> Option> { + match name { + "__new__" => { + // __new__(cls, field1, field2, ...) -> Self + let self_typevar = + BoundTypeVarInstance::synthetic_self(db, instance_ty, BindingContext::Synthetic); + let self_ty = Type::TypeVar(self_typevar); + + let variables = inherited_generic_context + .iter() + .flat_map(|ctx| ctx.variables(db)) + .chain(std::iter::once(self_typevar)); + + let generic_context = GenericContext::from_typevar_instances(db, variables); + + let first_parameter = Parameter::positional_or_keyword(Name::new_static("cls")) + .with_annotated_type(SubclassOfType::from(db, self_typevar)); + + let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { + Parameter::positional_or_keyword(field.name) + .with_annotated_type(field.ty) + .with_optional_default_type(field.default) + })); + + let signature = Signature::new_generic( + Some(generic_context), + Parameters::new(db, parameters), + self_ty, + ); + Some(Type::function_like_callable(db, signature)) + } + "_fields" => { + // _fields: tuple[Literal["field1"], Literal["field2"], ...] + let field_types = fields.map(|field| Type::string_literal(db, &field.name)); + Some(Type::heterogeneous_tuple(db, field_types)) + } + "__slots__" => { + // __slots__: tuple[()] - always empty for namedtuples + Some(Type::empty_tuple(db)) + } + "_replace" | "__replace__" => { + if name == "__replace__" && Program::get(db).python_version(db) < PythonVersion::PY313 { + return None; + } + + // _replace(self, *, field1=..., field2=...) -> Self + let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self( + db, + instance_ty, + BindingContext::Synthetic, + )); + + let first_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(self_ty); + + let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { + Parameter::keyword_only(field.name) + .with_annotated_type(field.ty) + .with_default_type(field.ty) + })); + + let signature = Signature::new(Parameters::new(db, parameters), self_ty); + Some(Type::function_like_callable(db, signature)) + } + "__init__" => { + // Namedtuples don't have a custom __init__. All construction happens in __new__. + None + } + _ => { + // Fall back to NamedTupleFallback for other synthesized methods. + KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_static()? + .own_class_member(db, inherited_generic_context, None, name) + .ignore_possibly_undefined() + } + } +} + +#[derive(Debug, salsa::Update, get_size2::GetSize, Clone, PartialEq, Eq, Hash)] +pub struct NamedTupleField<'db> { + pub(crate) name: Name, + pub(crate) ty: Type<'db>, + pub(crate) default: Option>, +} + +/// A namedtuple created via the functional form `namedtuple(name, fields)` or +/// `NamedTuple(name, fields)`. +/// +/// For example: +/// ```python +/// from collections import namedtuple +/// Point = namedtuple("Point", ["x", "y"]) +/// +/// from typing import NamedTuple +/// Person = NamedTuple("Person", [("name", str), ("age", int)]) +/// ``` +/// +/// The type of `Point` would be `type[Point]` where `Point` is a `DynamicNamedTupleLiteral`. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub struct DynamicNamedTupleLiteral<'db> { + /// The name of the namedtuple (from the first argument). + #[returns(ref)] + pub name: Name, + + /// The anchor for this dynamic namedtuple, providing stable identity. + /// + /// - `Definition`: The call is assigned to a variable. The definition + /// uniquely identifies this namedtuple and can be used to find the call. + /// - `ScopeOffset`: The call is "dangling" (not assigned). The offset + /// is relative to the enclosing scope's anchor node index. + #[returns(ref)] + pub anchor: DynamicNamedTupleAnchor<'db>, +} + +impl get_size2::GetSize for DynamicNamedTupleLiteral<'_> {} + +#[salsa::tracked] +impl<'db> DynamicNamedTupleLiteral<'db> { + /// Returns the definition where this namedtuple is created, if it was assigned to a variable. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + match self.anchor(db) { + DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } + | DynamicNamedTupleAnchor::TypingDefinition(definition) => Some(*definition), + DynamicNamedTupleAnchor::ScopeOffset { .. } => None, + } + } + + /// Returns the scope in which this dynamic class was created. + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + match self.anchor(db) { + DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } + | DynamicNamedTupleAnchor::TypingDefinition(definition) => definition.scope(db), + DynamicNamedTupleAnchor::ScopeOffset { scope, .. } => *scope, + } + } + + /// Returns an instance type for this dynamic namedtuple. + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + Type::instance(db, ClassType::NonGeneric(self.into())) + } + + /// Returns the range of the namedtuple call expression. + pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + let scope = self.scope(db); + let file = scope.file(db); + let module = parsed_module(db, file).load(db); + + match self.anchor(db) { + DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } + | DynamicNamedTupleAnchor::TypingDefinition(definition) => { + // For definitions, get the range from the definition's value. + // The namedtuple call is the value of the assignment. + definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments") + .range() + } + DynamicNamedTupleAnchor::ScopeOffset { offset, .. } => { + // For dangling calls, compute the absolute index from the offset. + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("anchor should not be NodeIndex::NONE"); + let absolute_index = NodeIndex::from(anchor_u32 + offset); + + // Get the node and return its range. + let node: &ast::ExprCall = module + .get_by_index(absolute_index) + .try_into() + .expect("scope offset should point to ExprCall"); + node.range() + } + } + } + + /// Returns a [`Span`] pointing to the namedtuple call expression. + pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) + } + + /// Compute the MRO for this namedtuple. + /// + /// The MRO is the MRO of the class's tuple base class, prepended by `self`. + /// For example, `namedtuple("Point", [("x", int), ("y", int)])` has the following MRO: + /// + /// 1. `` + /// 2. `` + /// 3. `` + /// 4. `` + /// 5. `` + /// 6. `` + /// 7. `` + /// 8. `typing.Protocol` + /// 9. `typing.Generic` + /// 10. `` + #[salsa::tracked( + returns(ref), + heap_size=ruff_memory_usage::heap_size, + cycle_initial=dynamic_namedtuple_mro_cycle_initial + )] + pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { + let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); + let tuple_class = self.tuple_base_class(db); + std::iter::once(self_base) + .chain(tuple_class.iter_mro(db)) + .collect() + } + + /// Get the metaclass of this dynamic namedtuple. + /// + /// Namedtuples always have `type` as their metaclass. + pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + let _ = self; + KnownClass::Type.to_class_literal(db) + } + + /// Compute the specialized tuple class that this namedtuple inherits from. + /// + /// For example, `namedtuple("Point", [("x", int), ("y", int)])` inherits from `tuple[int, int]`. + pub(crate) fn tuple_base_class(self, db: &'db dyn Db) -> ClassType<'db> { + // If fields are unknown, return `tuple[Unknown, ...]` to avoid false positives + // like index-out-of-bounds errors. + if !self.has_known_fields(db) { + return TupleType::homogeneous(db, Type::unknown()).to_class_type(db); + } + + let field_types = self.fields(db).iter().map(|field| field.ty); + TupleType::heterogeneous(db, field_types) + .map(|t| t.to_class_type(db)) + .unwrap_or_else(|| { + KnownClass::Tuple + .to_class_literal(db) + .as_class_literal() + .expect("tuple should be a class literal") + .default_specialization(db) + }) + } + + /// Look up an instance member defined directly on this class (not inherited). + /// + /// For dynamic namedtuples, instance members are the field names. + /// If fields are unknown (dynamic), returns `Any` for any attribute. + pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + for field in self.fields(db) { + if field.name == name { + return Member::definitely_declared(field.ty); + } + } + + if !self.has_known_fields(db) { + return Member::definitely_declared(Type::any()); + } + + Member::unbound() + } + + /// Look up an instance member by name (including superclasses). + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + // First check own instance members. + let result = self.own_instance_member(db, name); + if !result.is_undefined() { + return result.inner; + } + + // Fall back to the tuple base type for other attributes. + Type::instance(db, self.tuple_base_class(db)).instance_member(db, name) + } + + /// Look up a class-level member by name. + pub(crate) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + // First check synthesized members and fields. + let member = self.own_class_member(db, name); + if !member.is_undefined() { + return member.inner; + } + + // Fall back to tuple class members. + let result = self + .tuple_base_class(db) + .class_literal(db) + .class_member(db, name, policy); + + // If fields are unknown (dynamic) and the attribute wasn't found, + // return `Any` instead of failing. + if !self.has_known_fields(db) && result.place.is_undefined() { + return Place::bound(Type::any()).into(); + } + + result + } + + /// Look up a class-level member defined directly on this class (not inherited). + /// + /// This only checks synthesized members and field properties, without falling + /// back to tuple or other base classes. + pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + // Handle synthesized namedtuple attributes. + if let Some(ty) = self.synthesized_class_member(db, name) { + return Member::definitely_declared(ty); + } + + // Check if it's a field name (returns a property descriptor). + for field in self.fields(db) { + if field.name == name { + return Member::definitely_declared(create_field_property(db, field.ty)); + } + } + + Member::default() + } + + /// Generate synthesized class members for namedtuples. + fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option> { + let instance_ty = self.to_instance(db); + + // When fields are unknown, handle constructor and field-specific methods specially. + if !self.has_known_fields(db) { + match name { + // For constructors, return a gradual signature that accepts any arguments. + "__new__" | "__init__" => { + let signature = Signature::new(Parameters::gradual_form(), instance_ty); + return Some(Type::function_like_callable(db, signature)); + } + // For other field-specific methods, fall through to NamedTupleFallback. + "_fields" | "_replace" | "__replace__" => { + return KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_static()? + .own_class_member(db, None, None, name) + .ignore_possibly_undefined() + .map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: instance_ty, + }, + TypeContext::default(), + ) + }); + } + _ => {} + } + } + + let result = synthesize_namedtuple_class_member( + db, + name, + instance_ty, + self.fields(db).iter().cloned(), + None, + ); + // For fallback members from NamedTupleFallback, apply type mapping to handle + // `Self` types. The explicitly synthesized members (__new__, _fields, _replace, + // __replace__) don't need this mapping. + if matches!( + name, + "__new__" | "_fields" | "_replace" | "__replace__" | "__slots__" + ) { + result + } else { + result.map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: instance_ty, + }, + TypeContext::default(), + ) + }) + } + } + + fn spec(self, db: &'db dyn Db) -> NamedTupleSpec<'db> { + #[salsa::tracked(cycle_initial=deferred_spec_initial, heap_size=ruff_memory_usage::heap_size)] + fn deferred_spec<'db>(db: &'db dyn Db, definition: Definition<'db>) -> NamedTupleSpec<'db> { + let module = parsed_module(db, definition.file(db)).load(db); + let node = definition + .kind(db) + .value(&module) + .expect("Expected `NamedTuple` definition to be an assignment") + .as_call_expr() + .expect("Expected `NamedTuple` definition r.h.s. to be a call expression"); + match definition_expression_type(db, definition, &node.arguments.args[1]) { + Type::KnownInstance(KnownInstanceType::NamedTupleSpec(spec)) => spec, + _ => NamedTupleSpec::unknown(db), + } + } + + fn deferred_spec_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + _definition: Definition<'db>, + ) -> NamedTupleSpec<'db> { + NamedTupleSpec::unknown(db) + } + + match self.anchor(db) { + DynamicNamedTupleAnchor::CollectionsDefinition { spec, .. } + | DynamicNamedTupleAnchor::ScopeOffset { spec, .. } => *spec, + DynamicNamedTupleAnchor::TypingDefinition(definition) => deferred_spec(db, *definition), + } + } + + fn fields(self, db: &'db dyn Db) -> &'db [NamedTupleField<'db>] { + self.spec(db).fields(db) + } + + pub(super) fn has_known_fields(self, db: &'db dyn Db) -> bool { + self.spec(db).has_known_fields(db) + } +} + +fn dynamic_namedtuple_mro_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: DynamicNamedTupleLiteral<'db>, +) -> Mro<'db> { + Mro::from_error( + db, + ClassType::NonGeneric(ClassLiteral::DynamicNamedTuple(self_)), + ) +} + +/// Anchor for identifying a dynamic `namedtuple`/`NamedTuple` class literal. +/// +/// This enum provides stable identity for `DynamicNamedTupleLiteral` instances: +/// - For assigned calls, the `Definition` uniquely identifies the class. +/// - For dangling calls, a relative offset provides stable identity. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub enum DynamicNamedTupleAnchor<'db> { + /// We're dealing with a `collections.namedtuple()` call + /// that's assigned to a variable. + /// + /// The `Definition` uniquely identifies this class. The `namedtuple()` + /// call expression is the `value` of the assignment, so we can get its + /// range from the definition. + CollectionsDefinition { + definition: Definition<'db>, + spec: NamedTupleSpec<'db>, + }, + + /// We're dealing with a `typing.NamedTuple()` call + /// that's assigned to a variable. + /// + /// The `Definition` uniquely identifies this class. The `NamedTuple()` + /// call expression is the `value` of the assignment, so we can get its + /// range from the definition. + /// + /// Unlike the `CollectionsDefinition` variant, this variant does not + /// hold a `NamedTupleSpec`. This is because the spec for a + /// `typing.NamedTuple` call can contain forward references and recursive + /// references that must be evaluated lazily. The spec is computed + /// on-demand from the definition. + TypingDefinition(Definition<'db>), + + /// We're dealing with a `namedtuple()` or `NamedTuple` call that is + /// "dangling" (not assigned to a variable). + /// + /// The offset is relative to the enclosing scope's anchor node index. + /// For module scope, this is equivalent to an absolute index (anchor is 0). + /// + /// Dangling calls can always store the spec. They *can* contain + /// forward references if they appear in class bases: + /// + /// ```python + /// from typing import NamedTuple + /// + /// class F(NamedTuple("F", [("x", "F | None")]): + /// pass + /// ``` + /// + /// But this doesn't matter, because all class bases are deferred in their + /// entirety during type inference. + ScopeOffset { + scope: ScopeId<'db>, + offset: u32, + spec: NamedTupleSpec<'db>, + }, +} + +/// A specification describing the fields of a dynamic `namedtuple` +/// or `NamedTuple` class. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct NamedTupleSpec<'db> { + #[returns(deref)] + pub(crate) fields: Box<[NamedTupleField<'db>]>, + + pub(crate) has_known_fields: bool, +} + +impl<'db> NamedTupleSpec<'db> { + /// Create a [`NamedTupleSpec`] with the given fields. + pub(crate) fn known(db: &'db dyn Db, fields: Box<[NamedTupleField<'db>]>) -> Self { + Self::new(db, fields, true) + } + + /// Create a [`NamedTupleSpec`] that indicates a namedtuple class has unknown fields. + pub(crate) fn unknown(db: &'db dyn Db) -> Self { + Self::new(db, Box::default(), false) + } + + pub(crate) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + let fields = self + .fields(db) + .iter() + .map(|f| { + Some(NamedTupleField { + name: f.name.clone(), + ty: if nested { + f.ty.recursive_type_normalized_impl(db, div, nested)? + } else { + f.ty.recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div) + }, + default: None, + }) + }) + .collect::>>()?; + + Some(Self::new(db, fields, self.has_known_fields(db))) + } +} + +impl get_size2::GetSize for NamedTupleSpec<'_> {} + +/// Create a property type for a namedtuple field. +fn create_field_property<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Type<'db> { + let property_getter_signature = Signature::new( + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("self")))], + ), + field_ty, + ); + let property_getter = Type::single_callable(db, property_getter_signature); + let property = PropertyInstanceType::new(db, Some(property_getter), None); + Type::PropertyInstance(property) +} diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs new file mode 100644 index 0000000000000..972d3908a5aa5 --- /dev/null +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -0,0 +1,3129 @@ +use itertools::{Either, Itertools}; +use ruff_db::{ + diagnostic::Span, + files::File, + parsed::{ParsedModuleRef, parsed_module}, +}; +use ruff_python_ast as ast; +use ruff_python_ast::{PythonVersion, name::Name}; +use ruff_text_size::{Ranged, TextRange}; +use std::cell::RefCell; + +use crate::{ + Db, FxIndexMap, FxIndexSet, Program, TypeQualifiers, + place::{ + DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening, + place_from_bindings, place_from_declarations, + }, + semantic_index::{ + DeclarationWithConstraint, attribute_assignments, attribute_declarations, attribute_scopes, + definition::{Definition, DefinitionKind, DefinitionState, TargetKind}, + place_table, + scope::{Scope, ScopeId}, + semantic_index, + symbol::Symbol, + use_def_map, + }, + types::{ + ApplyTypeMappingVisitor, BoundTypeVarInstance, CallArguments, CallableType, ClassBase, + ClassLiteral, ClassType, DATACLASS_FLAGS, DataclassFlags, DataclassParams, GenericAlias, + GenericContext, KnownClass, KnownInstanceType, MaterializationKind, MemberLookupPolicy, + MetaclassCandidate, MetaclassTransformInfo, Parameter, Parameters, PropertyInstanceType, + Signature, SpecialFormType, StaticMroError, SubclassOfType, Truthiness, Type, TypeContext, + TypeMapping, TypeVarVariance, UnionBuilder, UnionType, + call::{CallError, CallErrorKind}, + callable::CallableTypeKind, + class::{ + ClassMemberResult, CodeGeneratorKind, DisjointBase, Field, FieldKind, + InstanceMemberResult, MetaclassError, MetaclassErrorKind, MethodDecorator, MroLookup, + NamedTupleField, SlotsKind, synthesize_namedtuple_class_member, + }, + context::InferContext, + declaration_type, definition_expression_type, determine_upper_bound, + diagnostic::INVALID_DATACLASS_OVERRIDE, + enums::{enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value}, + function::{ + DataclassTransformerFlags, DataclassTransformerParams, KnownFunction, + is_implicit_classmethod, is_implicit_staticmethod, + }, + generics::Specialization, + infer::infer_unpack_types, + infer_expression_type, + known_instance::DeprecatedInstance, + member::{Member, class_member}, + mro::{Mro, MroIterator}, + signatures::CallableSignature, + tuple::{Tuple, TupleSpec, TupleType}, + typed_dict::{TypedDictParams, typed_dict_params_from_class_def}, + variance::VarianceInferable, + visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}, + }, +}; + +/// Representation of a class definition statement in the AST: either a non-generic class, or a +/// generic class that has not been specialized. +/// +/// This does not in itself represent a type, but can be transformed into a [`ClassType`] that +/// does. (For generic classes, this requires specializing its generic context.) +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct StaticClassLiteral<'db> { + /// Name of the class at definition + #[returns(ref)] + pub(crate) name: Name, + + pub(crate) body_scope: ScopeId<'db>, + + pub(crate) known: Option, + + /// If this class is deprecated, this holds the deprecation message. + pub(crate) deprecated: Option>, + + pub(crate) type_check_only: bool, + + pub(crate) dataclass_params: Option>, + pub(crate) dataclass_transformer_params: Option>, + + /// Whether this class is decorated with `@functools.total_ordering` + pub(crate) total_ordering: bool, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for StaticClassLiteral<'_> {} + +fn generic_context_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: StaticClassLiteral<'db>, +) -> Option> { + None +} + +#[salsa::tracked] +impl<'db> StaticClassLiteral<'db> { + /// Return `true` if this class represents `known_class` + pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { + self.known(db) == Some(known_class) + } + + pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { + self.is_known(db, KnownClass::Tuple) + } + + /// Returns `true` if this class inherits from a functional namedtuple + /// (`DynamicNamedTupleLiteral`) that has unknown fields. + /// + /// When the base namedtuple's fields were determined dynamically (e.g., from a variable), + /// we can't synthesize precise method signatures and should fall back to `NamedTupleFallback`. + pub(crate) fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { + self.explicit_bases(db).iter().any(|base| match base { + Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) => { + !namedtuple.has_known_fields(db) + } + _ => false, + }) + } + + /// Returns `true` if this class is a dataclass-like class. + /// + /// This covers `@dataclass`-decorated classes, as well as classes created via + /// `dataclass_transform` (function-based, metaclass-based, and base-class-based). + pub(crate) fn is_dataclass_like(self, db: &'db dyn Db) -> bool { + matches!( + CodeGeneratorKind::from_class(db, ClassLiteral::Static(self), None), + Some(CodeGeneratorKind::DataclassLike(_)) + ) + } + + /// Returns a new [`StaticClassLiteral`] with the given dataclass params, preserving all other fields. + pub(crate) fn with_dataclass_params( + self, + db: &'db dyn Db, + dataclass_params: Option>, + ) -> Self { + Self::new( + db, + self.name(db).clone(), + self.body_scope(db), + self.known(db), + self.deprecated(db), + self.type_check_only(db), + dataclass_params, + self.dataclass_transformer_params(db), + self.total_ordering(db), + ) + } + + /// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`, + /// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if + /// synthesis is valid. + #[salsa::tracked] + pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + let body_scope = self.body_scope(db); + ["__lt__", "__le__", "__gt__", "__ge__"] + .iter() + .any(|method| !class_member(db, body_scope, method).is_undefined()) + } + + /// Returns `true` if any class in this class's MRO (excluding `object`) defines an ordering + /// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation. + pub(crate) fn has_ordering_method_in_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> bool { + self.total_ordering_root_method(db, specialization) + .is_some() + } + + /// Returns the type of the ordering method used by `@total_ordering`, if any. + /// + /// Following `functools.total_ordering` precedence, we prefer `__lt__` > `__le__` > `__gt__` > + /// `__ge__`, regardless of whether the method is defined locally or inherited. + /// + /// Note: We use direct scope lookups here to avoid infinite recursion + /// through `own_class_member` -> `own_synthesized_member`. + pub(super) fn total_ordering_root_method( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Option> { + const ORDERING_METHODS: [&str; 4] = ["__lt__", "__le__", "__gt__", "__ge__"]; + + for name in ORDERING_METHODS { + for base in self.iter_mro(db, specialization) { + let Some(base_class) = base.into_class() else { + continue; + }; + match base_class.class_literal(db) { + ClassLiteral::Static(base_literal) => { + if base_literal.is_known(db, KnownClass::Object) { + continue; + } + let member = class_member(db, base_literal.body_scope(db), name); + if let Some(ty) = member.ignore_possibly_undefined() { + let base_specialization = base_class + .static_class_literal(db) + .and_then(|(_, spec)| spec); + return Some(ty.apply_optional_specialization(db, base_specialization)); + } + } + ClassLiteral::Dynamic(dynamic) => { + // Dynamic classes (created with `type()`) can also define ordering methods + // in their namespace dict. + let member = dynamic.own_class_member(db, name); + if let Some(ty) = member.ignore_possibly_undefined() { + return Some(ty); + } + } + // Dynamic namedtuples don't define their own ordering methods. + ClassLiteral::DynamicNamedTuple(_) => {} + } + } + } + + None + } + + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code + // the knowledge that this class is not generic. + if self.is_known(db, KnownClass::VersionInfo) { + return None; + } + + // We've already verified that the class literal does not contain both a PEP-695 generic + // scope and a `typing.Generic` base class. + // + // Note that if a class has an explicit legacy generic context (by inheriting from + // `typing.Generic`), and also an implicit one (by inheriting from other generic classes, + // specialized by typevars), the explicit one takes precedence. + self.pep695_generic_context(db) + .or_else(|| self.legacy_generic_context(db)) + .or_else(|| self.inherited_legacy_generic_context(db)) + } + + pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + self.pep695_generic_context(db).is_some() + } + + #[salsa::tracked(cycle_initial=generic_context_cycle_initial, + heap_size=ruff_memory_usage::heap_size, + )] + pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option> { + let scope = self.body_scope(db); + let file = scope.file(db); + let parsed = parsed_module(db, file).load(db); + let class_def_node = scope.node(db).expect_class().node(&parsed); + class_def_node.type_params.as_ref().map(|type_params| { + let index = semantic_index(db, scope.file(db)); + let definition = index.expect_single_definition(class_def_node); + GenericContext::from_type_params(db, index, definition, type_params) + }) + } + + pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { + self.explicit_bases(db).iter().find_map(|base| match base { + Type::KnownInstance( + KnownInstanceType::SubscriptedGeneric(generic_context) + | KnownInstanceType::SubscriptedProtocol(generic_context), + ) => Some(*generic_context), + _ => None, + }) + } + + #[salsa::tracked(cycle_initial=generic_context_cycle_initial, + heap_size=ruff_memory_usage::heap_size, + )] + pub(crate) fn inherited_legacy_generic_context( + self, + db: &'db dyn Db, + ) -> Option> { + GenericContext::from_base_classes( + db, + self.definition(db), + self.explicit_bases(db) + .iter() + .copied() + .filter(|ty| matches!(ty, Type::GenericAlias(_))), + ) + } + + /// Returns all of the typevars that are referenced in this class's base class list. + /// (This is used to ensure that classes do not reference typevars from enclosing + /// generic contexts.) + pub(crate) fn typevars_referenced_in_bases( + self, + db: &'db dyn Db, + ) -> FxIndexSet> { + #[derive(Default)] + struct CollectTypeVars<'db> { + typevars: RefCell>>, + recursion_guard: TypeCollector<'db>, + } + + impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { + fn should_visit_lazy_type_attributes(&self) -> bool { + false + } + + fn visit_bound_type_var_type( + &self, + _db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, + ) { + self.typevars.borrow_mut().insert(bound_typevar); + } + + fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { + walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); + } + } + + let visitor = CollectTypeVars::default(); + for base in self.explicit_bases(db) { + visitor.visit_type(db, *base); + } + visitor.typevars.into_inner() + } + + /// Returns the generic context that should be inherited by any constructor methods of this class. + pub(super) fn inherited_generic_context(self, db: &'db dyn Db) -> Option> { + self.generic_context(db) + } + + pub(crate) fn file(self, db: &dyn Db) -> File { + self.body_scope(db).file(db) + } + + /// Return the original [`ast::StmtClassDef`] node associated with this class + /// + /// ## Note + /// Only call this function from queries in the same file or your + /// query depends on the AST of another file (bad!). + fn node<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef { + let scope = self.body_scope(db); + scope.node(db).expect_class().node(module) + } + + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + let body_scope = self.body_scope(db); + let index = semantic_index(db, body_scope.file(db)); + index.expect_single_definition(body_scope.node(db).expect_class()) + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> ClassType<'db> { + match self.generic_context(db) { + None => ClassType::NonGeneric(self.into()), + Some(generic_context) => { + let specialization = f(generic_context); + + ClassType::Generic(GenericAlias::new(db, self, specialization)) + } + } + } + + pub(crate) fn apply_optional_specialization( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + specialization + .unwrap_or_else(|| generic_context.default_specialization(db, self.known(db))) + }) + } + + pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context + .default_specialization(db, self.known(db)) + .materialize_impl( + db, + MaterializationKind::Top, + &ApplyTypeMappingVisitor::default(), + ) + }) + } + + /// Returns the default specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// applies the default specialization to the class's typevars. + pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context.default_specialization(db, self.known(db)) + }) + } + + /// Returns the unknown specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// maps each of the class's typevars to `Unknown`. + pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context.unknown_specialization(db) + }) + } + + /// Returns a specialization of this class where each typevar is mapped to itself. + pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context.identity_specialization(db) + }) + } + + /// Return an iterator over the inferred types of this class's *explicit* bases. + /// + /// Note that any class (except for `object`) that has no explicit + /// bases will implicitly inherit from `object` at runtime. Nonetheless, + /// this method does *not* include `object` in the bases it iterates over. + /// + /// ## Why is this a salsa query? + /// + /// This is a salsa query to short-circuit the invalidation + /// when the class's AST node changes. + /// + /// Were this not a salsa query, then the calling query + /// would depend on the class's AST and rerun for every change in that file. + #[salsa::tracked(returns(deref), cycle_initial=explicit_bases_cycle_initial, cycle_fn=explicit_bases_cycle_fn, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + tracing::trace!( + "StaticClassLiteral::explicit_bases_query: {}", + self.name(db) + ); + + let module = parsed_module(db, self.file(db)).load(db); + let class_stmt = self.node(db, &module); + + let class_definition = + semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + + match self.known(db) { + Some(KnownClass::VersionInfo) => { + let tuple_type = TupleType::new(db, &TupleSpec::version_info_spec(db)) + .expect("sys.version_info tuple spec should always be a valid tuple"); + + Box::new([ + definition_expression_type(db, class_definition, &class_stmt.bases()[0]), + Type::from(tuple_type.to_class_type(db)), + ]) + } + // Special-case `NotImplementedType`: typeshed says that it inherits from `Any`, + // but this causes more problems than it fixes. + Some(KnownClass::NotImplementedType) => Box::new([]), + _ => class_stmt + .bases() + .iter() + .flat_map(|base_node| { + if let ast::Expr::Starred(starred) = base_node { + let starred_ty = + definition_expression_type(db, class_definition, &starred.value); + // If the starred expression is a fixed-length tuple, unpack it. + if let Some(Tuple::Fixed(tuple)) = starred_ty + .tuple_instance_spec(db) + .map(std::borrow::Cow::into_owned) + { + return Either::Left(tuple.owned_elements().into_vec().into_iter()); + } + // Otherwise, we can't statically determine the bases. + Either::Right(std::iter::once(Type::unknown())) + } else { + Either::Right(std::iter::once(definition_expression_type( + db, + class_definition, + base_node, + ))) + } + }) + .collect(), + } + } + + /// Return `Some()` if this class is known to be a [`DisjointBase`], or `None` if it is not. + pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + if self + .known_function_decorators(db) + .contains(&KnownFunction::DisjointBase) + { + Some(DisjointBase::due_to_decorator(self)) + } else if SlotsKind::from(db, self) == SlotsKind::NotEmpty { + Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Static( + self, + ))) + } else { + None + } + } + + /// Iterate over this class's explicit bases, filtering out any bases that are not class + /// objects, and applying default specialization to any unspecialized generic class literals. + fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { + self.explicit_bases(db) + .iter() + .copied() + .filter_map(|ty| ty.to_class_type(db)) + } + + /// Determine if this class is a protocol. + /// + /// This method relies on the accuracy of the [`KnownClass::is_protocol`] method, + /// which hardcodes knowledge about certain special-cased classes. See the docs on + /// that method for why we do this rather than relying on generalised logic for all + /// classes, including the special-cased ones that are included in the [`KnownClass`] + /// enum. + pub(crate) fn is_protocol(self, db: &'db dyn Db) -> bool { + self.known(db) + .map(KnownClass::is_protocol) + .unwrap_or_else(|| { + // Iterate through the last three bases of the class + // searching for `Protocol` or `Protocol[]` in the bases list. + // + // If `Protocol` is present in the bases list of a valid protocol class, it must either: + // + // - be the last base + // - OR be the last-but-one base (with the final base being `Generic[]` or `object`) + // - OR be the last-but-two base (with the penultimate base being `Generic[]` + // and the final base being `object`) + self.explicit_bases(db).iter().rev().take(3).any(|base| { + matches!( + base, + Type::SpecialForm(SpecialFormType::Protocol) + | Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)) + ) + }) + }) + } + + /// Return the types of the decorators on this class + #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] + fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + tracing::trace!("StaticClassLiteral::decorators: {}", self.name(db)); + + let module = parsed_module(db, self.file(db)).load(db); + + let class_stmt = self.node(db, &module); + if class_stmt.decorator_list.is_empty() { + return Box::new([]); + } + + let class_definition = + semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + + class_stmt + .decorator_list + .iter() + .map(|decorator_node| { + definition_expression_type(db, class_definition, &decorator_node.expression) + }) + .collect() + } + + pub(crate) fn known_function_decorators( + self, + db: &'db dyn Db, + ) -> impl Iterator + 'db { + self.decorators(db) + .iter() + .filter_map(|deco| deco.as_function_literal()) + .filter_map(|decorator| decorator.known(db)) + } + + /// Iterate through the decorators on this class, returning the position of the first one + /// that matches the given predicate. + pub(super) fn find_decorator_position( + self, + db: &'db dyn Db, + predicate: impl Fn(Type<'db>) -> bool, + ) -> Option { + self.decorators(db) + .iter() + .position(|decorator| predicate(*decorator)) + } + + /// Iterate through the decorators on this class, returning the index of the first one + /// that is either `@dataclass` or `@dataclass(...)`. + pub(crate) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { + self.find_decorator_position(db, |ty| match ty { + Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass), + Type::DataclassDecorator(_) => true, + _ => false, + }) + } + + /// Is this class final? + pub(crate) fn is_final(self, db: &'db dyn Db) -> bool { + self.known_function_decorators(db) + .contains(&KnownFunction::Final) + || enum_metadata(db, ClassLiteral::Static(self)).is_some() + } + + /// Attempt to resolve the [method resolution order] ("MRO") for this class. + /// If the MRO is unresolvable, return an error indicating why the class's MRO + /// cannot be accurately determined. The error returned contains a fallback MRO + /// that will be used instead for the purposes of type inference. + /// + /// The MRO is the tuple of classes that can be retrieved as the `__mro__` + /// attribute on a class at runtime. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + #[salsa::tracked(returns(as_ref), cycle_initial=static_class_try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn try_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Result, StaticMroError<'db>> { + tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db)); + Mro::of_static_class(db, self, specialization) + } + + /// Iterate over the [method resolution order] ("MRO") of the class. + /// + /// If the MRO could not be accurately resolved, this method falls back to iterating + /// over an MRO that has the class directly inheriting from `Unknown`. Use + /// [`StaticClassLiteral::try_mro`] if you need to distinguish between the success and failure + /// cases rather than simply iterating over the inferred resolution order for the class. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(crate) fn iter_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> MroIterator<'db> { + MroIterator::new(db, ClassLiteral::Static(self), specialization) + } + + /// Return `true` if `other` is present in this class's MRO. + pub(super) fn is_subclass_of( + self, + db: &'db dyn Db, + specialization: Option>, + other: ClassType<'db>, + ) -> bool { + // `is_subclass_of` is checking the subtype relation, in which gradual types do not + // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. + self.iter_mro(db, specialization) + .contains(&ClassBase::Class(other)) + } + + /// Return `true` if this class constitutes a typed dict specification (inherits from + /// `typing.TypedDict`, either directly or indirectly). + #[salsa::tracked(cycle_initial=|_, _, _| false, heap_size=ruff_memory_usage::heap_size)] + pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { + if let Some(known) = self.known(db) { + return known.is_typed_dict_subclass(); + } + + self.iter_mro(db, None) + .any(|base| matches!(base, ClassBase::TypedDict)) + } + + /// Return `true` if this class is, or inherits from, a `NamedTuple` (inherits from + /// `typing.NamedTuple`, either directly or indirectly, including functional forms like + /// `NamedTuple("X", ...)`). + pub(crate) fn has_named_tuple_class_in_mro(self, db: &'db dyn Db) -> bool { + self.iter_mro(db, None) + .filter_map(ClassBase::into_class) + .any(|base| match base.class_literal(db) { + ClassLiteral::DynamicNamedTuple(_) => true, + ClassLiteral::Dynamic(_) => false, + ClassLiteral::Static(class) => class + .explicit_bases(db) + .contains(&Type::SpecialForm(SpecialFormType::NamedTuple)), + }) + } + + /// Compute `TypedDict` parameters dynamically based on MRO detection and AST parsing. + fn typed_dict_params(self, db: &'db dyn Db) -> Option { + if !self.is_typed_dict(db) { + return None; + } + + let module = parsed_module(db, self.file(db)).load(db); + let class_stmt = self.node(db, &module); + Some(typed_dict_params_from_class_def(class_stmt)) + } + + /// Returns dataclass params for this class, sourced from both dataclass params and dataclass + /// transform params + fn merged_dataclass_params( + self, + db: &'db dyn Db, + field_policy: CodeGeneratorKind<'db>, + ) -> (Option>, Option>) { + let dataclass_params = self.dataclass_params(db); + + let mut transformer_params = + if let CodeGeneratorKind::DataclassLike(Some(transformer_params)) = field_policy { + Some(DataclassParams::from_transformer_params( + db, + transformer_params, + )) + } else { + None + }; + + // Dataclass transformer flags can be overwritten using class arguments. + if let Some(transformer_params) = transformer_params.as_mut() { + if let Some(class_def) = self.definition(db).kind(db).as_class() { + let module = parsed_module(db, self.file(db)).load(db); + + if let Some(arguments) = &class_def.node(&module).arguments { + let mut flags = transformer_params.flags(db); + + for keyword in &arguments.keywords { + if let Some(arg_name) = &keyword.arg { + if let Some(is_set) = + keyword.value.as_boolean_literal_expr().map(|b| b.value) + { + for (flag_name, flag) in DATACLASS_FLAGS { + if arg_name.as_str() == *flag_name { + flags.set(*flag, is_set); + } + } + } + } + } + + *transformer_params = + DataclassParams::new(db, flags, transformer_params.field_specifiers(db)); + } + } + } + + (dataclass_params, transformer_params) + } + + /// Returns the effective frozen status of this class if it's a dataclass-like class. + /// + /// Returns `Some(true)` for a frozen dataclass-like class, `Some(false)` for a non-frozen one, + /// and `None` if the class is not a dataclass-like class, or if the dataclass is neither frozen + /// nor non-frozen. + pub(crate) fn is_frozen_dataclass(self, db: &'db dyn Db) -> Option { + // Check if this is a base-class-based transformer that has dataclass_transformer_params directly + // attached to it (because it is itself decorated with `@dataclass_transform`), or if this class + // has an explicit metaclass that is decorated with `@dataclass_transform`. + // + // In both cases, this signifies that this class is neither frozen nor non-frozen. + // + // See for details. + if self.dataclass_transformer_params(db).is_some() + || self + .try_metaclass(db) + .is_ok_and(|(_, info)| info.is_some_and(|i| i.from_explicit_metaclass)) + { + return None; + } + + if let field_policy @ CodeGeneratorKind::DataclassLike(_) = + CodeGeneratorKind::from_class(db, self.into(), None)? + { + // Otherwise, if this class is a dataclass-like class, determine its frozen status based on + // dataclass params and dataclass transformer params. + Some(self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN)) + } else { + None + } + } + + /// Checks if the given dataclass parameter flag is set for this class. + /// This checks both the `dataclass_params` and `transformer_params`. + fn has_dataclass_param( + self, + db: &'db dyn Db, + field_policy: CodeGeneratorKind<'db>, + param: DataclassFlags, + ) -> bool { + let (dataclass_params, transformer_params) = self.merged_dataclass_params(db, field_policy); + dataclass_params.is_some_and(|params| params.flags(db).contains(param)) + || transformer_params.is_some_and(|params| params.flags(db).contains(param)) + } + + /// Return the explicit `metaclass` of this class, if one is defined. + /// + /// ## Note + /// Only call this function from queries in the same file or your + /// query depends on the AST of another file (bad!). + fn explicit_metaclass(self, db: &'db dyn Db, module: &ParsedModuleRef) -> Option> { + let class_stmt = self.node(db, module); + let metaclass_node = &class_stmt + .arguments + .as_ref()? + .find_keyword("metaclass")? + .value; + + let class_definition = self.definition(db); + + Some(definition_expression_type( + db, + class_definition, + metaclass_node, + )) + } + + /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. + pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + self.try_metaclass(db) + .map(|(ty, _)| ty) + .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) + } + + /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. + #[salsa::tracked(cycle_initial=try_metaclass_cycle_initial, + heap_size=ruff_memory_usage::heap_size, + )] + pub(crate) fn try_metaclass( + self, + db: &'db dyn Db, + ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { + tracing::trace!("StaticClassLiteral::try_metaclass: {}", self.name(db)); + + // Identify the class's own metaclass (or take the first base class's metaclass). + let mut base_classes = self.fully_static_explicit_bases(db).peekable(); + + if base_classes.peek().is_some() && self.inheritance_cycle(db).is_some() { + // We emit diagnostics for cyclic class definitions elsewhere. + // Avoid attempting to infer the metaclass if the class is cyclically defined. + return Ok((SubclassOfType::subclass_of_unknown(), None)); + } + + if self.try_mro(db, None).is_err_and(StaticMroError::is_cycle) { + return Ok((SubclassOfType::subclass_of_unknown(), None)); + } + + let module = parsed_module(db, self.file(db)).load(db); + + let explicit_metaclass = self.explicit_metaclass(db, &module); + + // Generic metaclasses parameterized by type variables are not supported. + // `metaclass=Meta[int]` is fine, but `metaclass=Meta[T]` is not. + // See: https://typing.python.org/en/latest/spec/generics.html#generic-metaclasses + if let Some(Type::GenericAlias(alias)) = explicit_metaclass { + let specialization_has_typevars = alias + .specialization(db) + .types(db) + .iter() + .any(|ty| ty.has_typevar_or_typevar_instance(db)); + if specialization_has_typevars { + return Err(MetaclassError { + kind: MetaclassErrorKind::GenericMetaclass, + }); + } + } + + let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { + (metaclass, self) + } else if let Some(base_class) = base_classes.next() { + // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking. + let base_class_literal = base_class + .static_class_literal(db) + .map(|(lit, _)| lit) + .unwrap_or(self); + (base_class.metaclass(db), base_class_literal) + } else { + (KnownClass::Type.to_class_literal(db), self) + }; + + let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { + MetaclassCandidate { + metaclass: metaclass_ty, + explicit_metaclass_of: class_metaclass_was_from, + } + } else { + let name = Type::string_literal(db, self.name(db)); + let bases = Type::heterogeneous_tuple(db, self.explicit_bases(db)); + let namespace = KnownClass::Dict + .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); + + // TODO: Other keyword arguments? + let arguments = CallArguments::positional([name, bases, namespace]); + + let return_ty_result = match metaclass.try_call(db, &arguments) { + Ok(bindings) => Ok(bindings.return_type(db)), + + Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError { + kind: MetaclassErrorKind::NotCallable(bindings.callable_type()), + }), + + // TODO we should also check for binding errors that would indicate the metaclass + // does not accept the right arguments + Err(CallError(CallErrorKind::BindingError, bindings)) => { + Ok(bindings.return_type(db)) + } + + Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError { + kind: MetaclassErrorKind::PartlyNotCallable(metaclass), + }), + }; + + return return_ty_result.map(|ty| (ty.to_meta_type(db), None)); + }; + + // Reconcile all base classes' metaclasses with the candidate metaclass. + // + // See: + // - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass + // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 + for base_class in base_classes { + let metaclass = base_class.metaclass(db); + let Some(metaclass) = metaclass.to_class_type(db) else { + continue; + }; + // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking. + let base_class_literal = base_class + .static_class_literal(db) + .map(|(lit, _)| lit) + .unwrap_or(self); + if metaclass.is_subclass_of(db, candidate.metaclass) { + candidate = MetaclassCandidate { + metaclass, + explicit_metaclass_of: base_class_literal, + }; + continue; + } + if candidate.metaclass.is_subclass_of(db, metaclass) { + continue; + } + return Err(MetaclassError { + kind: MetaclassErrorKind::Conflict { + candidate1: candidate, + candidate2: MetaclassCandidate { + metaclass, + explicit_metaclass_of: base_class_literal, + }, + candidate1_is_base_class: explicit_metaclass.is_none(), + }, + }); + } + + let transform_info = candidate + .metaclass + .static_class_literal(db) + .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db)) + .map(|params| MetaclassTransformInfo { + params, + from_explicit_metaclass: candidate.explicit_metaclass_of == self, + }); + Ok((candidate.metaclass.into(), transform_info)) + } + + /// Returns the class member of this class named `name`. + /// + /// The member resolves to a member on the class itself or any of its proper superclasses. + /// + /// TODO: Should this be made private...? + pub(super) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { + match ty { + Type::Callable(callable_ty) => Type::Callable(CallableType::new( + db, + callable_ty.signatures(db), + CallableTypeKind::FunctionLike, + )), + Type::Union(union) => { + union.map(db, |element| into_function_like_callable(db, *element)) + } + Type::Intersection(intersection) => intersection + .map_positive(db, |element| into_function_like_callable(db, *element)), + _ => ty, + } + } + + let mut member = self.class_member_inner(db, None, name, policy); + + // We generally treat dunder attributes with `Callable` types as function-like callables. + // See `callables_as_descriptors.md` for more details. + if name.starts_with("__") && name.ends_with("__") { + member = member.map_type(|ty| into_function_like_callable(db, ty)); + } + + member + } + + pub(super) fn class_member_inner( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) + } + + pub(crate) fn class_member_from_mro( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + mro_iter: impl Iterator>, + ) -> PlaceAndQualifiers<'db> { + let result = MroLookup::new(db, mro_iter).class_member( + name, + policy, + self.inherited_generic_context(db), + self.is_known(db, KnownClass::Object), + ); + + match result { + ClassMemberResult::Done(result) => result.finalize(db), + + ClassMemberResult::TypedDict => KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("Will return Some() when called on class literal") + .map_type(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + None, + ClassBase::is_typed_dict, + ), + }, + TypeContext::default(), + ) + }), + } + } + + /// Returns the inferred type of the class member named `name`. Only bound members + /// or those marked as `ClassVars` are considered. + /// + /// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope + /// directly. Use [`StaticClassLiteral::class_member`] if you require a method that will + /// traverse through the MRO until it finds the member. + pub(super) fn own_class_member( + self, + db: &'db dyn Db, + inherited_generic_context: Option>, + specialization: Option>, + name: &str, + ) -> Member<'db> { + // Check if this class is dataclass-like (either via @dataclass or via dataclass_transform) + if matches!( + CodeGeneratorKind::from_class(db, self.into(), specialization), + Some(CodeGeneratorKind::DataclassLike(_)) + ) { + if name == "__dataclass_fields__" { + // Make this class look like a subclass of the `DataClassInstance` protocol + return Member { + inner: Place::declared(KnownClass::Dict.to_specialized_instance( + db, + &[ + KnownClass::Str.to_instance(db), + KnownClass::Field.to_specialized_instance(db, &[Type::any()]), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR), + }; + } else if name == "__dataclass_params__" { + // There is no typeshed class for this. For now, we model it as `Any`. + return Member { + inner: Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR), + }; + } + } + + if CodeGeneratorKind::NamedTuple.matches(db, self.into(), specialization) { + if let Some(field) = self + .own_fields(db, specialization, CodeGeneratorKind::NamedTuple) + .get(name) + { + let property_getter_signature = Signature::new( + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("self")))], + ), + field.declared_ty, + ); + let property_getter = Type::single_callable(db, property_getter_signature); + let property = PropertyInstanceType::new(db, Some(property_getter), None); + return Member::definitely_declared(Type::PropertyInstance(property)); + } + } + + let body_scope = self.body_scope(db); + let member = class_member(db, body_scope, name).map_type(|ty| { + // The `__new__` and `__init__` members of a non-specialized generic class are handled + // specially: they inherit the generic context of their class. That lets us treat them + // as generic functions when constructing the class, and infer the specialization of + // the class from the arguments that are passed in. + // + // We might decide to handle other class methods the same way, having them inherit the + // class's generic context, and performing type inference on calls to them to determine + // the specialization of the class. If we do that, we would update this to also apply + // to any method with a `@classmethod` decorator. (`__init__` would remain a special + // case, since it's an _instance_ method where we don't yet know the generic class's + // specialization.) + match (inherited_generic_context, ty, specialization, name) { + ( + Some(generic_context), + Type::FunctionLiteral(function), + Some(_), + "__new__" | "__init__", + ) => Type::FunctionLiteral( + function.with_inherited_generic_context(db, generic_context), + ), + _ => ty, + } + }); + + if member.is_undefined() { + if let Some(synthesized_member) = + self.own_synthesized_member(db, specialization, inherited_generic_context, name) + { + return Member::definitely_declared(synthesized_member); + } + // The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s. + return Self::implicit_attribute(db, body_scope, name, MethodDecorator::ClassMethod); + } + + // For dataclass-like classes, `KW_ONLY` sentinel fields are not real + // class attributes; they are markers used by the dataclass decorator to + // indicate that subsequent fields are keyword-only. Treat them as + // undefined so the MRO falls through to parent classes. + if member + .inner + .place + .unwidened_type() + .is_some_and(|ty| ty.is_instance_of(db, KnownClass::KwOnly)) + && CodeGeneratorKind::from_static_class(db, self, None) + .is_some_and(|policy| matches!(policy, CodeGeneratorKind::DataclassLike(_))) + { + return Member::unbound(); + } + + // For enum classes, `nonmember(value)` creates a non-member attribute. + // At runtime, the enum metaclass unwraps the value, so accessing the attribute + // returns the inner value, not the `nonmember` wrapper. + if let Some(ty) = member.inner.place.unwidened_type() { + if let Some(value_ty) = try_unwrap_nonmember_value(db, ty) { + if is_enum_class_by_inheritance(db, self) { + return Member::definitely_declared(value_ty); + } + } + } + + member + } + + /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or + /// a synthesized `__new__` method for a `NamedTuple`. + pub(crate) fn own_synthesized_member( + self, + db: &'db dyn Db, + specialization: Option>, + inherited_generic_context: Option>, + name: &str, + ) -> Option> { + // Handle `@functools.total_ordering`: synthesize comparison methods + // for classes that have `@total_ordering` and define at least one + // ordering method. The decorator requires at least one of __lt__, + // __le__, __gt__, or __ge__ to be defined (either in this class or + // inherited from a superclass, excluding `object`). + // + // Only synthesize methods that are not already defined in the MRO. + // Note: We use direct scope lookups here to avoid infinite recursion + // through `own_class_member` -> `own_synthesized_member`. + if self.total_ordering(db) + && matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") + && !self + .iter_mro(db, specialization) + .filter_map(ClassBase::into_class) + .filter_map(|class| class.static_class_literal(db)) + .filter(|(class, _)| !class.is_known(db, KnownClass::Object)) + .any(|(class, _)| { + class_member(db, class.body_scope(db), name) + .ignore_possibly_undefined() + .is_some() + }) + && self.has_ordering_method_in_mro(db, specialization) + && let Some(root_method_ty) = self.total_ordering_root_method(db, specialization) + && let Some(callables) = root_method_ty.try_upcast_to_callable(db) + { + let bool_ty = KnownClass::Bool.to_instance(db); + let synthesized_callables = callables.map(|callable| { + let signatures = CallableSignature::from_overloads( + callable.signatures(db).iter().map(|signature| { + // The generated methods return a union of the root method's return type + // and `bool`. This is because `@total_ordering` synthesizes methods like: + // def __gt__(self, other): return not (self == other or self < other) + // If `__lt__` returns `int`, then `__gt__` could return `int | bool`. + let return_ty = + UnionType::from_two_elements(db, signature.return_ty, bool_ty); + Signature::new_generic( + signature.generic_context, + signature.parameters().clone(), + return_ty, + ) + }), + ); + CallableType::new(db, signatures, CallableTypeKind::FunctionLike) + }); + + return Some(synthesized_callables.into_type(db)); + } + + let field_policy = CodeGeneratorKind::from_class(db, self.into(), specialization)?; + + let instance_ty = + Type::instance(db, self.apply_optional_specialization(db, specialization)); + + let signature_from_fields = |mut parameters: Vec<_>, return_ty: Type<'db>| { + for (field_name, field) in self.fields(db, specialization, field_policy) { + let (init, mut default_ty, kw_only, alias) = match &field.kind { + FieldKind::NamedTuple { default_ty } => (true, *default_ty, None, None), + FieldKind::Dataclass { + init, + default_ty, + kw_only, + alias, + .. + } => (*init, *default_ty, *kw_only, alias.as_ref()), + FieldKind::TypedDict { .. } => continue, + }; + let mut field_ty = field.declared_ty; + + if name == "__init__" && !init { + // Skip fields with `init=False` + continue; + } + + if field.is_kw_only_sentinel(db) { + // Attributes annotated with `dataclass.KW_ONLY` are not present in the synthesized + // `__init__` method; they are used to indicate that the following parameters are + // keyword-only. + continue; + } + + let dunder_set = field_ty.class_member(db, "__set__".into()); + if let Place::Defined(DefinedPlace { + ty: dunder_set, + definedness: Definedness::AlwaysDefined, + .. + }) = dunder_set.place + { + // The descriptor handling below is guarded by this not-dynamic check, because + // dynamic types like `Any` are valid (data) descriptors: since they have all + // possible attributes, they also have a (callable) `__set__` method. The + // problem is that we can't determine the type of the value parameter this way. + // Instead, we want to use the dynamic type itself in this case, so we skip the + // special descriptor handling. + if !dunder_set.is_dynamic() { + // This type of this attribute is a data descriptor. Instead of overwriting the + // descriptor attribute, data-classes will (implicitly) call the `__set__` method + // of the descriptor. This means that the synthesized `__init__` parameter for + // this attribute is determined by possible `value` parameter types with which + // the `__set__` method can be called. + // + // We union parameter types across overloads of a single callable, intersect + // callable bindings inside an intersection element, and union outer elements. + field_ty = dunder_set.bindings(db).map_types(db, |binding| { + let mut value_types = UnionBuilder::new(db); + let mut has_value_type = false; + for overload in binding { + if let Some(value_param) = + overload.signature.parameters().get_positional(2) + { + value_types = value_types.add(value_param.annotated_type()); + has_value_type = true; + } else if overload.signature.parameters().is_gradual() { + value_types = value_types.add(Type::unknown()); + has_value_type = true; + } + } + has_value_type.then(|| value_types.build()) + }); + + // The default value of the attribute is *not* determined by the right hand side + // of the class-body assignment. Instead, the runtime invokes `__get__` on the + // descriptor, as if it had been called on the class itself, i.e. it passes `None` + // for the `instance` argument. + + if let Some(ref mut default_ty) = default_ty { + *default_ty = default_ty + .try_call_dunder_get(db, None, Type::from(self)) + .map(|(return_ty, _)| return_ty) + .unwrap_or_else(Type::unknown); + } + } + } + + let is_kw_only = + matches!(name, "__replace__" | "_replace") || kw_only.unwrap_or(false); + + // Use the alias name if provided, otherwise use the field name + let parameter_name = + Name::new(alias.map(|alias| &**alias).unwrap_or(&**field_name)); + + let mut parameter = if is_kw_only { + Parameter::keyword_only(parameter_name) + } else { + Parameter::positional_or_keyword(parameter_name) + } + .with_annotated_type(field_ty); + + parameter = if matches!(name, "__replace__" | "_replace") { + // When replacing, we know there is a default value for the field + // (the value that is currently assigned to the field) + // assume this to be the declared type of the field + parameter.with_default_type(field_ty) + } else { + parameter.with_optional_default_type(default_ty) + }; + + parameters.push(parameter); + } + + // In the event that we have a mix of keyword-only and positional parameters, we need to sort them + // so that the keyword-only parameters appear after positional parameters. + parameters.sort_by_key(Parameter::is_keyword_only); + + let signature = match name { + "__new__" | "__init__" => Signature::new_generic( + inherited_generic_context.or_else(|| self.inherited_generic_context(db)), + Parameters::new(db, parameters), + return_ty, + ), + _ => Signature::new(Parameters::new(db, parameters), return_ty), + }; + Some(Type::function_like_callable(db, signature)) + }; + + match (field_policy, name) { + (CodeGeneratorKind::DataclassLike(_), "__init__") => { + if !self.has_dataclass_param(db, field_policy, DataclassFlags::INIT) { + return None; + } + + let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + // TODO: could be `Self`. + .with_annotated_type(instance_ty); + signature_from_fields(vec![self_parameter], Type::none(db)) + } + ( + CodeGeneratorKind::NamedTuple, + "__new__" | "__init__" | "_replace" | "__replace__" | "_fields", + ) if self.namedtuple_base_has_unknown_fields(db) => { + // When the namedtuple base has unknown fields, fall back to NamedTupleFallback + // which has generic signatures that accept any arguments. + KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_static()? + .own_class_member(db, inherited_generic_context, None, name) + .ignore_possibly_undefined() + .map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: instance_ty, + }, + TypeContext::default(), + ) + }) + } + ( + CodeGeneratorKind::NamedTuple, + "__new__" | "_replace" | "__replace__" | "_fields" | "__slots__", + ) => { + let fields = self.fields(db, specialization, field_policy); + let fields_iter = fields.iter().map(|(name, field)| { + let default_ty = match &field.kind { + FieldKind::NamedTuple { default_ty } => *default_ty, + _ => None, + }; + NamedTupleField { + name: name.clone(), + ty: field.declared_ty, + default: default_ty, + } + }); + synthesize_namedtuple_class_member( + db, + name, + instance_ty, + fields_iter, + specialization.map(|s| s.generic_context(db)), + ) + } + (CodeGeneratorKind::DataclassLike(_), "__lt__" | "__le__" | "__gt__" | "__ge__") => { + if !self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) { + return None; + } + + let signature = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("self")) + // TODO: could be `Self`. + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("other")) + // TODO: could be `Self`. + .with_annotated_type(instance_ty), + ], + ), + KnownClass::Bool.to_instance(db), + ); + + Some(Type::function_like_callable(db, signature)) + } + (CodeGeneratorKind::DataclassLike(_), "__hash__") => { + let unsafe_hash = + self.has_dataclass_param(db, field_policy, DataclassFlags::UNSAFE_HASH); + let frozen = self.has_dataclass_param(db, field_policy, DataclassFlags::FROZEN); + let eq = self.has_dataclass_param(db, field_policy, DataclassFlags::EQ); + + if unsafe_hash || (frozen && eq) { + let signature = Signature::new( + Parameters::new( + db, + [Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty)], + ), + KnownClass::Int.to_instance(db), + ); + + Some(Type::function_like_callable(db, signature)) + } else if eq && !frozen { + Some(Type::none(db)) + } else { + // No `__hash__` is generated, fall back to `object.__hash__` + None + } + } + (CodeGeneratorKind::DataclassLike(_), "__match_args__") + if Program::get(db).python_version(db) >= PythonVersion::PY310 => + { + if !self.has_dataclass_param(db, field_policy, DataclassFlags::MATCH_ARGS) { + return None; + } + + let kw_only_default = + self.has_dataclass_param(db, field_policy, DataclassFlags::KW_ONLY); + + let fields = self.fields(db, specialization, field_policy); + let match_args = fields + .iter() + .filter(|(_, field)| { + if let FieldKind::Dataclass { init, kw_only, .. } = &field.kind { + *init && !kw_only.unwrap_or(kw_only_default) + } else { + false + } + }) + .map(|(name, _)| Type::string_literal(db, name)); + Some(Type::heterogeneous_tuple(db, match_args)) + } + (CodeGeneratorKind::DataclassLike(_), "__weakref__") + if Program::get(db).python_version(db) >= PythonVersion::PY311 => + { + if !self.has_dataclass_param(db, field_policy, DataclassFlags::WEAKREF_SLOT) + || !self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) + { + return None; + } + + // This could probably be `weakref | None`, but it does not seem important enough to + // model it precisely. + Some(UnionType::from_two_elements( + db, + Type::any(), + Type::none(db), + )) + } + (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { + KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_static()? + .own_class_member(db, self.inherited_generic_context(db), None, name) + .ignore_possibly_undefined() + .map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + specialization, + |base| { + base.into_class() + .is_some_and(|c| c.is_known(db, KnownClass::Tuple)) + }, + ), + }, + TypeContext::default(), + ) + }) + } + (CodeGeneratorKind::DataclassLike(_), "__replace__") + if Program::get(db).python_version(db) >= PythonVersion::PY313 => + { + let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty); + + signature_from_fields(vec![self_parameter], instance_ty) + } + (CodeGeneratorKind::DataclassLike(_), "__setattr__") => { + if self.is_frozen_dataclass(db) == Some(true) { + let signature = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("name")), + Parameter::positional_or_keyword(Name::new_static("value")), + ], + ), + Type::Never, + ); + + return Some(Type::function_like_callable(db, signature)); + } + None + } + (CodeGeneratorKind::DataclassLike(_), "__slots__") + if Program::get(db).python_version(db) >= PythonVersion::PY310 => + { + self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) + .then(|| { + let fields = self.fields(db, specialization, field_policy); + let slots = fields.keys().map(|name| Type::string_literal(db, name)); + Type::heterogeneous_tuple(db, slots) + }) + } + (CodeGeneratorKind::TypedDict, "__setitem__") => { + let fields = self.fields(db, specialization, field_policy); + + // Add (key type, value type) overloads for all TypedDict items ("fields") that are not read-only: + + let mut writeable_fields = fields + .iter() + .filter(|(_, field)| !field.is_read_only()) + .peekable(); + + if writeable_fields.peek().is_none() { + // If there are no writeable fields, synthesize a `__setitem__` that takes + // a `key` of type `Never` to signal that no keys are accepted. This leads + // to slightly more user-friendly error messages compared to returning an + // empty overload set. + return Some(Type::Callable(CallableType::new( + db, + CallableSignature::single(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(Type::Never), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::any()), + ], + ), + Type::none(db), + )), + CallableTypeKind::FunctionLike, + ))); + } + + let overloads = writeable_fields.map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(field.declared_ty), + ], + ), + Type::none(db), + ) + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "__getitem__") => { + let fields = self.fields(db, specialization, field_policy); + + // Add (key -> value type) overloads for all TypedDict items ("fields"): + let overloads = fields.iter().map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), + field.declared_ty, + ) + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "__delitem__") => { + let fields = self.fields(db, specialization, field_policy); + + // Only non-required fields can be deleted. Required fields cannot be deleted + // because that would violate the TypedDict's structural type. + let mut deletable_fields = fields + .iter() + .filter(|(_, field)| !field.is_required()) + .peekable(); + + if deletable_fields.peek().is_none() { + // If there are no deletable fields (all fields are required), synthesize a + // `__delitem__` that takes a `key` of type `Never` to signal that no keys + // can be deleted. + return Some(Type::Callable(CallableType::new( + db, + CallableSignature::single(Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(Type::Never), + ], + ), + Type::none(db), + )), + CallableTypeKind::FunctionLike, + ))); + } + + // Otherwise, add overloads for all deletable fields. + let overloads = deletable_fields.map(|(name, _field)| { + let key_type = Type::string_literal(db, name); + + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), + Type::none(db), + ) + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "get") => { + let overloads = self + .fields(db, specialization, field_policy) + .iter() + .flat_map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + // For a required key, `.get()` always returns the value type. For a non-required key, + // `.get()` returns the union of the value type and the type of the default argument + // (which defaults to `None`). + + // TODO: For now, we use two overloads here. They can be merged into a single function + // once the generics solver takes default arguments into account. + + let get_sig = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), + if field.is_required() { + field.declared_ty + } else { + UnionType::from_two_elements(db, field.declared_ty, Type::none(db)) + }, + ); + + let t_default = BoundTypeVarInstance::synthetic( + db, + Name::new_static("T"), + TypeVarVariance::Covariant, + ); + + let get_with_default_sig = Signature::new_generic( + Some(GenericContext::from_typevar_instances(db, [t_default])), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), + if field.is_required() { + field.declared_ty + } else { + UnionType::from_two_elements( + db, + field.declared_ty, + Type::TypeVar(t_default), + ) + }, + ); + + [get_sig, get_with_default_sig] + }) + // Fallback overloads for unknown keys + .chain(std::iter::once({ + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + ], + ), + UnionType::from_two_elements(db, Type::unknown(), Type::none(db)), + ) + })) + .chain(std::iter::once({ + let t_default = BoundTypeVarInstance::synthetic( + db, + Name::new_static("T"), + TypeVarVariance::Covariant, + ); + + Signature::new_generic( + Some(GenericContext::from_typevar_instances(db, [t_default])), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), + UnionType::from_two_elements( + db, + Type::unknown(), + Type::TypeVar(t_default), + ), + ) + })); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "pop") => { + let fields = self.fields(db, specialization, field_policy); + let overloads = fields + .iter() + .filter(|(_, field)| { + // Only synthesize `pop` for fields that are not required. + !field.is_required() + }) + .flat_map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + // TODO: Similar to above: consider merging these two overloads into one + + // `.pop()` without default + let pop_sig = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), + field.declared_ty, + ); + + // `.pop()` with a default value + let t_default = BoundTypeVarInstance::synthetic( + db, + Name::new_static("T"), + TypeVarVariance::Covariant, + ); + + let pop_with_default_sig = Signature::new_generic( + Some(GenericContext::from_typevar_instances(db, [t_default])), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), + UnionType::from_two_elements( + db, + field.declared_ty, + Type::TypeVar(t_default), + ), + ); + + [pop_sig, pop_with_default_sig] + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "setdefault") => { + let fields = self.fields(db, specialization, field_policy); + let overloads = fields.iter().map(|(name, field)| { + let key_type = Type::string_literal(db, name); + + // `setdefault` always returns the field type + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(field.declared_ty), + ], + ), + field.declared_ty, + ) + }); + + Some(Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(overloads), + CallableTypeKind::FunctionLike, + ))) + } + (CodeGeneratorKind::TypedDict, "update") => { + // TODO: synthesize a set of overloads with precise types + let signature = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::variadic(Name::new_static("args")), + Parameter::keyword_variadic(Name::new_static("kwargs")), + ], + ), + Type::none(db), + ); + + Some(Type::function_like_callable(db, signature)) + } + _ => None, + } + } + + /// Member lookup for classes that inherit from `typing.TypedDict`. + /// + /// This is implemented as a separate method because the item definitions on a `TypedDict`-based + /// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`, + /// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`. + pub(crate) fn typed_dict_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + if let Some(member) = self.own_synthesized_member(db, specialization, None, name) { + Place::bound(member).into() + } else { + KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal") + .map_type(|ty| + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + specialization, + ClassBase::is_typed_dict + ) + }, + TypeContext::default(), + ) + ) + } + } + + /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. + /// + /// See [`StaticClassLiteral::own_fields`] for more details. + #[salsa::tracked( + returns(ref), + cycle_initial=|_, _, _, _, _| FxIndexMap::default(), + heap_size=get_size2::GetSize::get_heap_size)] + pub(crate) fn fields( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind<'db>, + ) -> FxIndexMap> { + if field_policy == CodeGeneratorKind::NamedTuple { + // NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the + // fields of this class only. + return self.own_fields(db, specialization, field_policy); + } + + let matching_classes_in_mro: Vec<(StaticClassLiteral<'db>, Option>)> = + self.iter_mro(db, specialization) + .filter_map(|superclass| { + let class = superclass.into_class()?; + // Dynamic classes don't have fields (no class body). + let (class_literal, specialization) = class.static_class_literal(db)?; + if field_policy.matches(db, class_literal.into(), specialization) { + Some((class_literal, specialization)) + } else { + None + } + }) + // We need to collect into a `Vec` here because we iterate the MRO in reverse order + .collect(); + + matching_classes_in_mro + .into_iter() + .rev() + .flat_map(|(class, specialization)| class.own_fields(db, specialization, field_policy)) + // KW_ONLY sentinels are markers, not real fields. Exclude them so + // they cannot shadow an inherited field with the same name. + .filter(|(_, field)| !field.is_kw_only_sentinel(db)) + // We collect into a FxOrderMap here to deduplicate attributes + .collect() + } + + pub(crate) fn validate_members(self, context: &InferContext<'db, '_>) { + let db = context.db(); + let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { + return; + }; + let class_body_scope = self.body_scope(db); + let table = place_table(db, class_body_scope); + let use_def = use_def_map(db, class_body_scope); + for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { + let result = place_from_declarations(db, declarations.clone()); + let attr = result.ignore_conflicting_declarations(); + let symbol = table.symbol(symbol_id); + let name = symbol.name(); + + let Some(Type::FunctionLiteral(literal)) = attr.place.ignore_possibly_undefined() + else { + continue; + }; + + match name.as_str() { + "__setattr__" | "__delattr__" => { + if let CodeGeneratorKind::DataclassLike(_) = field_policy + && self.is_frozen_dataclass(db) == Some(true) + { + if let Some(builder) = context.report_lint( + &INVALID_DATACLASS_OVERRIDE, + literal.node(db, context.file(), context.module()), + ) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot overwrite attribute `{}` in frozen dataclass `{}`", + name, + self.name(db) + )); + diagnostic.info(name); + } + } + } + "__lt__" | "__le__" | "__gt__" | "__ge__" => { + if let CodeGeneratorKind::DataclassLike(_) = field_policy + && self.has_dataclass_param(db, field_policy, DataclassFlags::ORDER) + { + if let Some(builder) = context.report_lint( + &INVALID_DATACLASS_OVERRIDE, + literal.node(db, context.file(), context.module()), + ) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot overwrite attribute `{}` in dataclass `{}` with `order=True`", + name, + self.name(db) + )); + diagnostic.info(name); + } + } + } + _ => {} + } + } + } + + /// Returns a map of all annotated attributes defined in the body of this class. + /// This extends the `__annotations__` attribute at runtime by also including default values + /// and computed field properties. + /// + /// For a class body like + /// ```py + /// @dataclass(kw_only=True) + /// class C: + /// x: int + /// y: str = "hello" + /// z: float = field(kw_only=False, default=1.0) + /// ``` + /// we return a map `{"x": Field, "y": Field, "z": Field}` where each `Field` contains + /// the annotated type, default value (if any), and field properties. + /// + /// **Important**: The returned `Field` objects represent our full understanding of the fields, + /// including properties inherited from class-level dataclass parameters (like `kw_only=True`) + /// and dataclass-transform parameters (like `kw_only_default=True`). They do not represent + /// only what is explicitly specified in each field definition. + pub(crate) fn own_fields( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind, + ) -> FxIndexMap> { + let mut attributes = FxIndexMap::default(); + + let class_body_scope = self.body_scope(db); + let table = place_table(db, class_body_scope); + + let use_def = use_def_map(db, class_body_scope); + + let typed_dict_params = self.typed_dict_params(db); + let mut kw_only_sentinel_field_seen = false; + + for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { + // Here, we exclude all declarations that are not annotated assignments. We need this because + // things like function definitions and nested classes would otherwise be considered dataclass + // fields. The check is too broad in the sense that it also excludes (weird) constructs where + // a symbol would have multiple declarations, one of which is an annotated assignment. If we + // want to improve this, we could instead pass a definition-kind filter to the use-def map + // query, or to the `symbol_from_declarations` call below. Doing so would potentially require + // us to generate a union of `__init__` methods. + if !declarations + .clone() + .all(|DeclarationWithConstraint { declaration, .. }| { + declaration.is_undefined_or(|declaration| { + matches!( + declaration.kind(db), + DefinitionKind::AnnotatedAssignment(..) + ) + }) + }) + { + continue; + } + + let symbol = table.symbol(symbol_id); + + let result = place_from_declarations(db, declarations.clone()); + let first_declaration = result.first_declaration; + let attr = result.ignore_conflicting_declarations(); + if attr.is_class_var() { + continue; + } + + if let Some(attr_ty) = attr.place.ignore_possibly_undefined() { + let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); + let mut default_ty = place_from_bindings(db, bindings) + .place + .ignore_possibly_undefined(); + + default_ty = + default_ty.map(|ty| ty.apply_optional_specialization(db, specialization)); + + let mut init = true; + let mut kw_only = None; + let mut alias = None; + if let Some(Type::KnownInstance(KnownInstanceType::Field(field))) = default_ty { + default_ty = field.default_type(db); + if self + .dataclass_params(db) + .map(|params| params.field_specifiers(db).is_empty()) + .unwrap_or(false) + { + // This happens when constructing a `dataclass` with a `dataclass_transform` + // without defining the `field_specifiers`, meaning it should ignore + // `dataclasses.field` and `dataclasses.Field`. + } else { + init = field.init(db); + kw_only = field.kw_only(db); + alias = field.alias(db); + } + } + + let kind = match field_policy { + CodeGeneratorKind::NamedTuple => FieldKind::NamedTuple { default_ty }, + CodeGeneratorKind::DataclassLike(_) => FieldKind::Dataclass { + default_ty, + init_only: attr.is_init_var(), + init, + kw_only, + alias, + }, + CodeGeneratorKind::TypedDict => { + let is_required = if attr.is_required() { + // Explicit Required[T] annotation - always required + true + } else if attr.is_not_required() { + // Explicit NotRequired[T] annotation - never required + false + } else { + // No explicit qualifier - use class default (`total` parameter) + typed_dict_params + .expect("TypedDictParams should be available for CodeGeneratorKind::TypedDict") + .contains(TypedDictParams::TOTAL) + }; + + FieldKind::TypedDict { + is_required, + is_read_only: attr.is_read_only(), + } + } + }; + + let mut field = Field { + declared_ty: attr_ty.apply_optional_specialization(db, specialization), + kind, + first_declaration, + }; + + // Check if this is a KW_ONLY sentinel and mark subsequent fields as keyword-only + if field.is_kw_only_sentinel(db) { + kw_only_sentinel_field_seen = true; + } + + // If no explicit kw_only setting and we've seen KW_ONLY sentinel, mark as keyword-only + if kw_only_sentinel_field_seen { + if let FieldKind::Dataclass { + kw_only: ref mut kw @ None, + .. + } = field.kind + { + *kw = Some(true); + } + } + + // Resolve the kw_only to the class-level default. This ensures that when fields + // are inherited by child classes, they use their defining class's kw_only default. + if let FieldKind::Dataclass { + kw_only: ref mut kw @ None, + .. + } = field.kind + { + let class_kw_only_default = self + .dataclass_params(db) + .is_some_and(|params| params.flags(db).contains(DataclassFlags::KW_ONLY)) + // TODO this next part should not be necessary, if we were properly + // initializing `dataclass_params` from the dataclass-transform params, for + // metaclass and base-class-based dataclass-transformers. + || matches!( + field_policy, + CodeGeneratorKind::DataclassLike(Some(transformer_params)) + if transformer_params.flags(db).contains(DataclassTransformerFlags::KW_ONLY_DEFAULT) + ); + *kw = Some(class_kw_only_default); + } + + attributes.insert(symbol.name().clone(), field); + } + } + + attributes + } + + /// Look up an instance attribute (available in `__dict__`) of the given name. + /// + /// See [`Type::instance_member`] for more details. + pub(super) fn instance_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + if self.is_typed_dict(db) { + return Place::Undefined.into(); + } + + match MroLookup::new(db, self.iter_mro(db, specialization)).instance_member(name) { + InstanceMemberResult::Done(result) => result, + InstanceMemberResult::TypedDict => KnownClass::TypedDictFallback + .to_instance(db) + .instance_member(db, name) + .map_type(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: Type::instance(db, self.unknown_specialization(db)), + }, + TypeContext::default(), + ) + }), + } + } + + /// Tries to find declarations/bindings of an attribute named `name` that are only + /// "implicitly" defined (`self.x = …`, `cls.x = …`) in a method of the class that + /// corresponds to `class_body_scope`. The `target_method_decorator` parameter is + /// used to skip methods that do not have the expected decorator. + fn implicit_attribute( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: &str, + target_method_decorator: MethodDecorator, + ) -> Member<'db> { + Self::implicit_attribute_inner( + db, + class_body_scope, + name.to_string(), + target_method_decorator, + ) + } + + #[salsa::tracked( + cycle_fn=implicit_attribute_cycle_recover, + cycle_initial=implicit_attribute_initial, + heap_size=ruff_memory_usage::heap_size, + )] + pub(super) fn implicit_attribute_inner( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: String, + target_method_decorator: MethodDecorator, + ) -> Member<'db> { + // If we do not see any declarations of an attribute, neither in the class body nor in + // any method, we build a union of `Unknown` with the inferred types of all bindings of + // that attribute. We include `Unknown` in that union to account for the fact that the + // attribute might be externally modified. + let mut union_of_inferred_types = UnionBuilder::new(db); + let mut qualifiers = TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE; + + let mut is_attribute_bound = false; + + let file = class_body_scope.file(db); + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let class_map = use_def_map(db, class_body_scope); + let class_table = place_table(db, class_body_scope); + let is_valid_scope = |method_scope: &Scope| { + let Some(method_def) = method_scope.node().as_function() else { + return true; + }; + + // Check the decorators directly on the AST node to determine if this method + // is a classmethod or staticmethod. This is more reliable than checking the + // final evaluated type, which may be wrapped by other decorators like @cache. + let function_node = method_def.node(&module); + let definition = index.expect_single_definition(method_def); + + let mut is_classmethod = false; + let mut is_staticmethod = false; + + for decorator in &function_node.decorator_list { + let decorator_ty = + definition_expression_type(db, definition, &decorator.expression); + if let Type::ClassLiteral(class) = decorator_ty { + match class.known(db) { + Some(KnownClass::Classmethod) => is_classmethod = true, + Some(KnownClass::Staticmethod) => is_staticmethod = true, + _ => {} + } + } + } + + // Also check for implicit classmethods/staticmethods based on method name + let method_name = function_node.name.as_str(); + if is_implicit_classmethod(method_name) { + is_classmethod = true; + } + if is_implicit_staticmethod(method_name) { + is_staticmethod = true; + } + + match target_method_decorator { + MethodDecorator::None => !is_classmethod && !is_staticmethod, + MethodDecorator::ClassMethod => is_classmethod, + MethodDecorator::StaticMethod => is_staticmethod, + } + }; + + // First check declarations + for (attribute_declarations, method_scope_id) in + attribute_declarations(db, class_body_scope, &name) + { + let method_scope = index.scope(method_scope_id); + if !is_valid_scope(method_scope) { + continue; + } + + for attribute_declaration in attribute_declarations { + let DefinitionState::Defined(declaration) = attribute_declaration.declaration + else { + continue; + }; + + let DefinitionKind::AnnotatedAssignment(assignment) = declaration.kind(db) else { + continue; + }; + + // We found an annotated assignment of one of the following forms (using 'self' in these + // examples, but we support arbitrary names for the first parameters of methods): + // + // self.name: + // self.name: = … + + let annotation = declaration_type(db, declaration); + let annotation = Place::declared(annotation.inner).with_qualifiers( + annotation.qualifiers | TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE, + ); + + if let Some(all_qualifiers) = annotation.is_bare_final() { + if let Some(value) = assignment.value(&module) { + // If we see an annotated assignment with a bare `Final` as in + // `self.SOME_CONSTANT: Final = 1`, infer the type from the value + // on the right-hand side. + + let inferred_ty = infer_expression_type( + db, + index.expression(value), + TypeContext::default(), + ); + return Member { + inner: Place::bound(inferred_ty).with_qualifiers(all_qualifiers), + }; + } + + // If there is no right-hand side, just record that we saw a `Final` qualifier + qualifiers |= all_qualifiers; + continue; + } + + return Member { inner: annotation }; + } + } + + if !qualifiers.contains(TypeQualifiers::FINAL) { + union_of_inferred_types = union_of_inferred_types.add(Type::unknown()); + } + + for (attribute_assignments, attribute_binding_scope_id) in + attribute_assignments(db, class_body_scope, &name) + { + let binding_scope = index.scope(attribute_binding_scope_id); + if !is_valid_scope(binding_scope) { + continue; + } + + let scope_for_reachability_analysis = { + if binding_scope.node().as_function().is_some() { + binding_scope + } else if binding_scope.is_eager() { + let mut eager_scope_parent = binding_scope; + while eager_scope_parent.is_eager() + && let Some(parent) = eager_scope_parent.parent() + { + eager_scope_parent = index.scope(parent); + } + eager_scope_parent + } else { + binding_scope + } + }; + + // The attribute assignment inherits the reachability of the method which contains it + let is_method_reachable = + if let Some(method_def) = scope_for_reachability_analysis.node().as_function() { + let method = index.expect_single_definition(method_def); + let method_place = class_table + .symbol_id(&method_def.node(&module).name) + .unwrap(); + class_map + .reachable_symbol_bindings(method_place) + .find_map(|bind| { + (bind.binding.is_defined_and(|def| def == method)) + .then(|| class_map.binding_reachability(db, &bind)) + }) + .unwrap_or(Truthiness::AlwaysFalse) + } else { + Truthiness::AlwaysFalse + }; + if is_method_reachable.is_always_false() { + continue; + } + + for attribute_assignment in attribute_assignments { + if let DefinitionState::Undefined = attribute_assignment.binding { + continue; + } + + let DefinitionState::Defined(binding) = attribute_assignment.binding else { + continue; + }; + + if !is_method_reachable.is_always_false() { + is_attribute_bound = true; + } + + match binding.kind(db) { + DefinitionKind::AnnotatedAssignment(_) => { + // Annotated assignments were handled above. This branch is not + // unreachable (because of the `continue` above), but there is + // nothing to do here. + } + DefinitionKind::Assignment(assign) => { + match assign.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // .., self.name, .. = + // (.., self.name, ..) = + // [.., self.name, ..] = + + let unpacked = infer_unpack_types(db, unpack); + + let inferred_ty = unpacked.expression_type(assign.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an un-annotated attribute assignment of the form: + // + // self.name = + + let inferred_ty = infer_expression_type( + db, + index.expression(assign.value(&module)), + TypeContext::default(), + ); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::For(for_stmt) => { + match for_stmt.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // for .., self.name, .. in : + + let unpacked = infer_unpack_types(db, unpack); + let inferred_ty = + unpacked.expression_type(for_stmt.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // for self.name in : + + let iterable_ty = infer_expression_type( + db, + index.expression(for_stmt.iterable(&module)), + TypeContext::default(), + ); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = + iterable_ty.iterate(db).homogeneous_element_type(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::WithItem(with_item) => { + match with_item.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // with as .., self.name, ..: + + let unpacked = infer_unpack_types(db, unpack); + let inferred_ty = + unpacked.expression_type(with_item.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // with as self.name: + + let context_ty = infer_expression_type( + db, + index.expression(with_item.context_expr(&module)), + TypeContext::default(), + ); + let inferred_ty = if with_item.is_async() { + context_ty.aenter(db) + } else { + context_ty.enter(db) + }; + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::Comprehension(comprehension) => { + match comprehension.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // [... for .., self.name, .. in ] + + let unpacked = infer_unpack_types(db, unpack); + + let inferred_ty = + unpacked.expression_type(comprehension.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // [... for self.name in ] + + let iterable_ty = infer_expression_type( + db, + index.expression(comprehension.iterable(&module)), + TypeContext::default(), + ); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = + iterable_ty.iterate(db).homogeneous_element_type(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::AugmentedAssignment(_) => { + // TODO: + } + DefinitionKind::NamedExpression(_) => { + // A named expression whose target is an attribute is syntactically prohibited + } + _ => {} + } + } + } + + Member { + inner: if is_attribute_bound { + Place::bound(union_of_inferred_types.build()).with_qualifiers(qualifiers) + } else { + Place::Undefined.with_qualifiers(qualifiers) + }, + } + } + + /// A helper function for `instance_member` that looks up the `name` attribute only on + /// this class, not on its superclasses. + pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + // TODO: There are many things that are not yet implemented here: + // - `typing.Final` + // - Proper diagnostics + + let body_scope = self.body_scope(db); + let table = place_table(db, body_scope); + + if let Some(symbol_id) = table.symbol_id(name) { + let use_def = use_def_map(db, body_scope); + + let declarations = use_def.end_of_scope_symbol_declarations(symbol_id); + let declared_and_qualifiers = + place_from_declarations(db, declarations).ignore_conflicting_declarations(); + + match declared_and_qualifiers { + PlaceAndQualifiers { + place: + mut declared @ Place::Defined(DefinedPlace { + ty: declared_ty, + definedness: declaredness, + .. + }), + qualifiers, + } => { + // For the purpose of finding instance attributes, ignore `ClassVar` + // declarations: + if qualifiers.contains(TypeQualifiers::CLASS_VAR) { + declared = Place::Undefined; + } + + if qualifiers.contains(TypeQualifiers::INIT_VAR) { + // We ignore `InitVar` declarations on the class body, unless that attribute is overwritten + // by an implicit assignment in a method + if Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) + .is_undefined() + { + return Member::unbound(); + } + } + + // `KW_ONLY` sentinels are markers, not real instance attributes. + if declared_ty.is_instance_of(db, KnownClass::KwOnly) + && CodeGeneratorKind::from_static_class(db, self, None).is_some_and( + |policy| matches!(policy, CodeGeneratorKind::DataclassLike(_)), + ) + { + return Member::unbound(); + } + + // The attribute is declared in the class body. + + let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); + let inferred = place_from_bindings(db, bindings).place; + let has_binding = !inferred.is_undefined(); + + if has_binding { + // The attribute is declared and bound in the class body. + + if let Some(implicit_ty) = + Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) + .ignore_possibly_undefined() + { + if declaredness == Definedness::AlwaysDefined { + // If a symbol is definitely declared, and we see + // attribute assignments in methods of the class, + // we trust the declared type. + Member { + inner: declared.with_qualifiers(qualifiers), + } + } else { + Member { + inner: Place::Defined(DefinedPlace { + ty: UnionType::from_two_elements( + db, + declared_ty, + implicit_ty, + ), + origin: TypeOrigin::Declared, + definedness: declaredness, + widening: Widening::None, + }) + .with_qualifiers(qualifiers), + } + } + } else if self.is_own_dataclass_instance_field(db, name) + && declared_ty + .class_member(db, "__get__".into()) + .place + .is_undefined() + { + // For dataclass-like classes, declared fields are assigned + // by the synthesized `__init__`, so they are instance + // attributes even without an explicit `self.x = ...` + // assignment in a method body. + // + // However, if the declared type is a descriptor (has + // `__get__`), we return unbound so that the descriptor + // protocol in `member_lookup_with_policy` can resolve + // the attribute type through `__get__`. + Member { + inner: declared.with_qualifiers(qualifiers), + } + } else { + // The symbol is declared and bound in the class body, + // but we did not find any attribute assignments in + // methods of the class. This means that the attribute + // has a class-level default value, but it would not be + // found in a `__dict__` lookup. + + Member::unbound() + } + } else { + // The attribute is declared but not bound in the class body. + // We take this as a sign that this is intended to be a pure + // instance attribute, and we trust the declared type, unless + // it is possibly-undeclared. In the latter case, we also + // union with the inferred type from attribute assignments. + + if declaredness == Definedness::AlwaysDefined { + Member { + inner: declared.with_qualifiers(qualifiers), + } + } else { + if let Some(implicit_ty) = Self::implicit_attribute( + db, + body_scope, + name, + MethodDecorator::None, + ) + .inner + .place + .ignore_possibly_undefined() + { + Member { + inner: Place::Defined(DefinedPlace { + ty: UnionType::from_two_elements( + db, + declared_ty, + implicit_ty, + ), + origin: TypeOrigin::Declared, + definedness: declaredness, + widening: Widening::None, + }) + .with_qualifiers(qualifiers), + } + } else { + Member { + inner: declared.with_qualifiers(qualifiers), + } + } + } + } + } + + PlaceAndQualifiers { + place: Place::Undefined, + qualifiers: _, + } => { + // The attribute is not *declared* in the class body. It could still be declared/bound + // in a method. + + Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) + } + } + } else { + // This attribute is neither declared nor bound in the class body. + // It could still be implicitly defined in a method. + + Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) + } + } + + /// Returns `true` if `name` is a non-init-only field directly declared on this + /// dataclass (i.e., a field that corresponds to an instance attribute). + /// + /// This is used to decide whether a bare class-body annotation like `x: int` + /// should be treated as defining an instance attribute: dataclass fields are + /// implicitly assigned in `__init__`, so they behave as instance attributes + /// even though no explicit binding exists in the class body. + fn is_own_dataclass_instance_field(self, db: &'db dyn Db, name: &str) -> bool { + let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { + return false; + }; + if !matches!(field_policy, CodeGeneratorKind::DataclassLike(_)) { + return false; + } + + let fields = self.own_fields(db, None, field_policy); + let Some(field) = fields.get(name) else { + return false; + }; + matches!( + field.kind, + FieldKind::Dataclass { + init_only: false, + .. + } + ) + } + + pub(super) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { + Type::instance(db, ClassType::NonGeneric(self.into())) + } + + /// Return this class' involvement in an inheritance cycle, if any. + /// + /// A class definition like this will fail at runtime, + /// but we must be resilient to it or we could panic. + #[salsa::tracked(cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { + /// Return `true` if the class is cyclically defined. + /// + /// Also, populates `visited_classes` with all base classes of `self`. + fn is_cyclically_defined_recursive<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, + classes_on_stack: &mut FxIndexSet>, + visited_classes: &mut FxIndexSet>, + ) -> bool { + let mut result = false; + for explicit_base in class.explicit_bases(db) { + let explicit_base_class_literal = match explicit_base { + Type::ClassLiteral(class_literal) => class_literal.as_static(), + Type::GenericAlias(generic_alias) => Some(generic_alias.origin(db)), + _ => continue, + }; + let Some(explicit_base_class_literal) = explicit_base_class_literal else { + continue; + }; + if !classes_on_stack.insert(explicit_base_class_literal) { + return true; + } + + if visited_classes.insert(explicit_base_class_literal) { + // If we find a cycle, keep searching to check if we can reach the starting class. + result |= is_cyclically_defined_recursive( + db, + explicit_base_class_literal, + classes_on_stack, + visited_classes, + ); + } + classes_on_stack.pop(); + } + result + } + + tracing::trace!("Class::inheritance_cycle: {}", self.name(db)); + + let visited_classes = &mut FxIndexSet::default(); + if !is_cyclically_defined_recursive(db, self, &mut FxIndexSet::default(), visited_classes) { + None + } else if visited_classes.contains(&self) { + Some(InheritanceCycle::Participant) + } else { + Some(InheritanceCycle::Inherited) + } + } + + /// Returns a [`Span`] with the range of the class's header. + /// + /// See [`Self::header_range`] for more details. + pub(crate) fn header_span(self, db: &'db dyn Db) -> Span { + Span::from(self.file(db)).with_range(self.header_range(db)) + } + + /// Returns the range of the class's "header": the class name + /// and any arguments passed to the `class` statement. E.g. + /// + /// ```ignore + /// class Foo(Bar, metaclass=Baz): ... + /// ^^^^^^^^^^^^^^^^^^^^^^^ + /// ``` + pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + let class_scope = self.body_scope(db); + let module = parsed_module(db, class_scope.file(db)).load(db); + let class_node = class_scope.node(db).expect_class().node(&module); + let class_name = &class_node.name; + TextRange::new( + class_name.start(), + class_node + .arguments + .as_deref() + .map(Ranged::end) + .unwrap_or_else(|| class_name.end()), + ) + } +} + +#[salsa::tracked] +impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { + #[salsa::tracked(cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] + fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { + let typevar_in_generic_context = self + .generic_context(db) + .is_some_and(|generic_context| generic_context.variables(db).contains(&typevar)); + + if !typevar_in_generic_context { + return TypeVarVariance::Bivariant; + } + let class_body_scope = self.body_scope(db); + + let file = class_body_scope.file(db); + let index = semantic_index(db, file); + + let explicit_bases_variances = self + .explicit_bases(db) + .iter() + .map(|class| class.variance_of(db, typevar)); + + let default_attribute_variance = { + let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self.into(), None); + // Python 3.13 introduced a synthesized `__replace__` method on dataclasses which uses + // their field types in contravariant position, thus meaning a frozen dataclass must + // still be invariant in its field types. Other synthesized methods on dataclasses are + // not considered here, since they don't use field types in their signatures. TODO: + // ideally we'd have a single source of truth for information about synthesized + // methods, so we just look them up normally and don't hardcode this knowledge here. + let is_frozen_dataclass = Program::get(db).python_version(db) <= PythonVersion::PY312 + && self + .dataclass_params(db) + .is_some_and(|params| params.flags(db).contains(DataclassFlags::FROZEN)); + if is_namedtuple || is_frozen_dataclass { + TypeVarVariance::Covariant + } else { + TypeVarVariance::Invariant + } + }; + + let init_name: &Name = &"__init__".into(); + let new_name: &Name = &"__new__".into(); + + let use_def_map = index.use_def_map(class_body_scope.file_scope_id(db)); + let table = place_table(db, class_body_scope); + let attribute_places_and_qualifiers = + use_def_map + .all_end_of_scope_symbol_declarations() + .map(|(symbol_id, declarations)| { + let place_and_qual = + place_from_declarations(db, declarations).ignore_conflicting_declarations(); + (symbol_id, place_and_qual) + }) + .chain(use_def_map.all_end_of_scope_symbol_bindings().map( + |(symbol_id, bindings)| { + (symbol_id, place_from_bindings(db, bindings).place.into()) + }, + )) + .filter_map(|(symbol_id, place_and_qual)| { + if let Some(name) = table.place(symbol_id).as_symbol().map(Symbol::name) { + (![init_name, new_name].contains(&name)) + .then_some((name.to_string(), place_and_qual)) + } else { + None + } + }); + + // Dataclasses can have some additional synthesized methods (`__eq__`, `__hash__`, + // `__lt__`, etc.) but none of these will have field types type variables in their signatures, so we + // don't need to consider them for variance. + + let attribute_names = attribute_scopes(db, self.body_scope(db)) + .flat_map(|function_scope_id| { + index + .place_table(function_scope_id) + .members() + .filter_map(|member| member.as_instance_attribute()) + .filter(|name| *name != init_name && *name != new_name) + .map(std::string::ToString::to_string) + .collect::>() + }) + .dedup(); + + let attribute_variances = attribute_names + .map(|name| { + let place_and_quals = self.own_instance_member(db, &name).inner; + (name, place_and_quals) + }) + .chain(attribute_places_and_qualifiers) + .dedup() + .filter_map(|(name, place_and_qual)| { + place_and_qual.ignore_possibly_undefined().map(|ty| { + let variance = if place_and_qual + .qualifiers + // `CLASS_VAR || FINAL` is really `all()`, but + // we want to be robust against new qualifiers + .intersects(TypeQualifiers::CLASS_VAR | TypeQualifiers::FINAL) + // We don't allow mutation of methods or properties + || ty.is_function_literal() + || ty.is_property_instance() + // Underscore-prefixed attributes are assumed not to be externally mutated + || name.starts_with('_') + { + // CLASS_VAR: class vars generally shouldn't contain the + // type variable, but they could if it's a + // callable type. They can't be mutated on instances. + // + // FINAL: final attributes are immutable, and thus covariant + TypeVarVariance::Covariant + } else { + default_attribute_variance + }; + ty.with_polarity(variance).variance_of(db, typevar) + }) + }); + + attribute_variances + .chain(explicit_bases_variances) + .collect() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub(crate) enum InheritanceCycle { + /// The class is cyclically defined and is a participant in the cycle. + /// i.e., it inherits either directly or indirectly from itself. + Participant, + /// The class inherits from a class that is a `Participant` in an inheritance cycle, + /// but is not itself a participant. + Inherited, +} + +impl InheritanceCycle { + pub(crate) const fn is_participant(self) -> bool { + matches!(self, InheritanceCycle::Participant) + } +} + +fn explicit_bases_cycle_initial<'db>( + db: &'db dyn Db, + id: salsa::Id, + literal: StaticClassLiteral<'db>, +) -> Box<[Type<'db>]> { + let module = parsed_module(db, literal.file(db)).load(db); + let class_stmt = literal.node(db, &module); + // Try to produce a list of `Divergent` types of the right length. However, if one or more of + // the bases is a starred expression, we don't know how many entries that will eventually + // expand to. + vec![Type::divergent(id); class_stmt.bases().len()].into_boxed_slice() +} + +fn explicit_bases_cycle_fn<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + previous: &[Type<'db>], + current: Box<[Type<'db>]>, + _literal: StaticClassLiteral<'db>, +) -> Box<[Type<'db>]> { + if previous.len() == current.len() { + // As long as the length of bases hasn't changed, use the same "monotonic widening" + // strategy that we use with most types, to avoid oscillations. + current + .iter() + .zip(previous.iter()) + .map(|(curr, prev)| curr.cycle_normalized(db, *prev, cycle)) + .collect() + } else { + // The length of bases has changed, presumably because we expanded a starred expression. We + // don't do "monotonic widening" here, because we don't want to make assumptions about + // which previous entries correspond to which current ones. An oscillation here would be + // unfortunate, but maybe only pathological programs can trigger such a thing. + current + } +} + +fn static_class_try_mro_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: StaticClassLiteral<'db>, + specialization: Option>, +) -> Result, StaticMroError<'db>> { + Err(StaticMroError::cycle( + db, + self_.apply_optional_specialization(db, specialization), + )) +} + +#[allow(clippy::unnecessary_wraps)] +fn try_metaclass_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self_: StaticClassLiteral<'db>, +) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { + Err(MetaclassError { + kind: MetaclassErrorKind::Cycle, + }) +} + +fn implicit_attribute_initial<'db>( + _db: &'db dyn Db, + id: salsa::Id, + _class_body_scope: ScopeId<'db>, + _name: String, + _target_method_decorator: MethodDecorator, +) -> Member<'db> { + Member { + inner: Place::bound(Type::divergent(id)).into(), + } +} + +#[allow(clippy::too_many_arguments)] +fn implicit_attribute_cycle_recover<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + previous_member: &Member<'db>, + member: Member<'db>, + _class_body_scope: ScopeId<'db>, + _name: String, + _target_method_decorator: MethodDecorator, +) -> Member<'db> { + let inner = member + .inner + .cycle_normalized(db, previous_member.inner, cycle); + Member { inner } +} From 015217e485344cd417b38a535457b319fe068465 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 4 Mar 2026 22:06:50 +0000 Subject: [PATCH 200/261] Update conformance suite commit hash (#23719) Co-authored-by: Claude --- .github/workflows/typing_conformance.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index f3ef000d07a2e..6ff5d7b8572d9 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -34,7 +34,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 RUST_BACKTRACE: 1 - CONFORMANCE_SUITE_COMMIT: 5b5f2f89bd19462f4707400f0437ab5a48d88bb3 + CONFORMANCE_SUITE_COMMIT: 56b7944b90d428d7014b4550452c2a187c70f482 PYTHON_VERSION: 3.12 jobs: From b1f43cb4c92132e0723206afd37b6c45bca2b0d1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 4 Mar 2026 19:18:10 -0500 Subject: [PATCH 201/261] [ty] Fix panic on incomplete except handlers (#23708) ## Summary We no longer assume that `ExceptHandlerExceptHandler` always has a definition. Closes https://github.com/astral-sh/ty/issues/2401. --- crates/ty_ide/src/completion.rs | 37 ++++++++++++++++++ crates/ty_ide/src/goto.rs | 22 ++++++----- crates/ty_ide/src/hover.rs | 14 +++++++ .../ty_python_semantic/src/semantic_model.rs | 39 +++++++++++++++---- 4 files changed, 95 insertions(+), 17 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index eea8dcdd5a682..40ef8c47da5ef 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -5334,6 +5334,43 @@ except Type: ); } + // Ref: https://github.com/astral-sh/ty/issues/2401 + #[test] + fn no_panic_incomplete_except_handler() { + let builder = completion_test_builder( + "\ +try: + print() +except # Trigger completion/hover here +", + ); + + assert_snapshot!( + builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(), + @"", + ); + } + + // Ref: https://github.com/astral-sh/ty/issues/2401 + #[test] + fn incomplete_except_handler_uses_enclosing_scope() { + completion_test_builder( + "\ +def f(): + sentinel = 1 + try: + print() + except as err: + pass +", + ) + .skip_keywords() + .skip_builtins() + .skip_auto_import() + .build() + .contains("sentinel"); + } + // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_global1() { diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index d00e2b93e7c80..dae5ec67cdd60 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -331,7 +331,7 @@ impl GotoTarget<'_> { GotoTarget::ImportSymbolAlias { alias, .. } | GotoTarget::ImportModuleAlias { alias, .. } | GotoTarget::ImportExportedName { alias, .. } => alias.inferred_type(model), - GotoTarget::ExceptVariable(except) => except.inferred_type(model), + GotoTarget::ExceptVariable(except) => model.except_handler_type(except), GotoTarget::KeywordArgument { keyword, .. } => keyword.value.inferred_type(model), // When asking the type of a callable, usually you want the callable itself? // (i.e. the type of `MyClass` in `MyClass()` is `` and not `() -> MyClass`) @@ -515,11 +515,9 @@ impl GotoTarget<'_> { )), // For exception variables, they are their own definitions (like parameters) - GotoTarget::ExceptVariable(except_handler) => { - Some(vec![ResolvedDefinition::Definition( - except_handler.definition(model), - )]) - } + GotoTarget::ExceptVariable(except_handler) => model + .except_handler_definition(except_handler) + .map(|definition| vec![ResolvedDefinition::Definition(definition)]), // Patterns are glorified assignments but we have to look them up by ident // because they're not expressions @@ -949,9 +947,10 @@ impl GotoTarget<'_> { None } - Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => { - Some(GotoTarget::ExceptVariable(handler)) - } + Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => handler + .name + .is_some() + .then_some(GotoTarget::ExceptVariable(handler)), Some(AnyNodeRef::Keyword(keyword)) => { // Find the containing call expression from the ancestor chain let call_expression = covering_node @@ -1139,7 +1138,10 @@ impl Ranged for GotoTarget<'_> { } => *component_range, GotoTarget::StringAnnotationSubexpr { subrange, .. } => *subrange, GotoTarget::ImportModuleAlias { asname, .. } => asname.range, - GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range, + GotoTarget::ExceptVariable(except) => except + .name + .as_ref() + .map_or(except.range(), |name| name.range), GotoTarget::KeywordArgument { keyword, .. } => keyword.arg.as_ref().unwrap().range, GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range, GotoTarget::PatternKeywordArgument(keyword) => keyword.attr.range, diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index a448a7b2fc9c3..c36fc71f0fbbb 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -4952,6 +4952,20 @@ def function(): "); } + // Ref: https://github.com/astral-sh/ty/issues/2401 + #[test] + fn hover_incomplete_except_handler() { + let test = cursor_test( + "\ +try: + print() +except # Trigger completion/hover here +", + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + impl CursorTest { fn hover(&self) -> String { use std::fmt::Write; diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 999750e27682b..2aaadea41a811 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -304,12 +304,15 @@ impl<'db> SemanticModel<'db> { .scope(self.db) .file_scope_id(self.db), ), - ast::AnyNodeRef::ExceptHandlerExceptHandler(handler) => Some( - handler - .definition(self) - .scope(self.db) - .file_scope_id(self.db), - ), + ast::AnyNodeRef::ExceptHandlerExceptHandler(handler) => self + .except_handler_definition(handler) + .map(|definition| definition.scope(self.db).file_scope_id(self.db)) + .or_else(|| { + handler.type_.as_deref().and_then(|handled_exceptions| { + index.try_expression_scope_id(handled_exceptions) + }) + }) + .or(Some(FileScopeId::global())), ast::AnyNodeRef::TypeParamTypeVar(var) => { Some(var.definition(self).scope(self.db).file_scope_id(self.db)) } @@ -325,6 +328,29 @@ impl<'db> SemanticModel<'db> { } } + /// Returns the definition for an exception-handler variable. + /// + /// Exception handlers only have a definition when they bind a name (`except E as name:`). + pub fn except_handler_definition( + &self, + handler: &ast::ExceptHandlerExceptHandler, + ) -> Option> { + handler.name.as_ref()?; + let index = semantic_index(self.db, self.file); + Some(index.expect_single_definition(handler)) + } + + /// Returns the inferred type of an exception-handler variable. + /// + /// Exception handlers only bind a variable when they have a name (`except E as name:`). + pub fn except_handler_type( + &self, + handler: &ast::ExceptHandlerExceptHandler, + ) -> Option> { + let definition = self.except_handler_definition(handler)?; + Some(binding_type(self.db, definition)) + } + /// Get a "safe" [`ast::AnyNodeRef`] to use for referring to the given (sub-)AST node. /// /// If we're analyzing a string annotation, it will return the string literal's node. @@ -641,7 +667,6 @@ impl_binding_has_ty_def!(ast::StmtFunctionDef); impl_binding_has_ty_def!(ast::StmtClassDef); impl_binding_has_ty_def!(ast::Parameter); impl_binding_has_ty_def!(ast::ParameterWithDefault); -impl_binding_has_ty_def!(ast::ExceptHandlerExceptHandler); impl_binding_has_ty_def!(ast::TypeParamTypeVar); impl HasType for ast::Alias { From 50f8602fd53b3fbeba4560260de35440894040d0 Mon Sep 17 00:00:00 2001 From: GiGaGon <107241144+MeGaGiGaGon@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:49:24 -0800 Subject: [PATCH 202/261] [ty] Add quotes to related issues links (#23720) --- crates/ruff_dev/src/generate_ty_rules.rs | 2 +- crates/ty/docs/rules.md | 226 +++++++++++------------ 2 files changed, 114 insertions(+), 114 deletions(-) diff --git a/crates/ruff_dev/src/generate_ty_rules.rs b/crates/ruff_dev/src/generate_ty_rules.rs index 87b76e56e9900..3b0cb51496410 100644 --- a/crates/ruff_dev/src/generate_ty_rules.rs +++ b/crates/ruff_dev/src/generate_ty_rules.rs @@ -116,7 +116,7 @@ fn generate_markdown() -> String { r#" Default level: {level} · {status_text} · -Related issues · +Related issues · View source diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index c4672536a4fdc..ad0e4e0a9ff9f 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -7,7 +7,7 @@ Default level: error · Added in 0.0.13 · -Related issues · +Related issues · View source @@ -48,7 +48,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · -Related issues · +Related issues · View source @@ -89,7 +89,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · -Related issues · +Related issues · View source @@ -125,7 +125,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -156,7 +156,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · -Related issues · +Related issues · View source @@ -205,7 +205,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -229,7 +229,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · -Related issues · +Related issues · View source @@ -260,7 +260,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -292,7 +292,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -323,7 +323,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -355,7 +355,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -387,7 +387,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · -Related issues · +Related issues · View source @@ -415,7 +415,7 @@ type B = A Default level: error · Preview (since 1.0.0) · -Related issues · +Related issues · View source @@ -447,7 +447,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · -Related issues · +Related issues · View source @@ -474,7 +474,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -503,7 +503,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -530,7 +530,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · -Related issues · +Related issues · View source @@ -568,7 +568,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · -Related issues · +Related issues · View source @@ -614,7 +614,7 @@ def bar() -> str: Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -639,7 +639,7 @@ def foo() -> "intt\b": ... Default level: error · Added in 0.0.20 · -Related issues · +Related issues · View source @@ -671,7 +671,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · -Related issues · +Related issues · View source @@ -704,7 +704,7 @@ MY_CONSTANT: Final[int] = 1 Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -735,7 +735,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -766,7 +766,7 @@ a = 20 / 0 # ty: ignore[division-by-zero] Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -797,7 +797,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -827,7 +827,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -853,7 +853,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · -Related issues · +Related issues · View source @@ -887,7 +887,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · -Related issues · +Related issues · View source @@ -976,7 +976,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1003,7 +1003,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1031,7 +1031,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1065,7 +1065,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · -Related issues · +Related issues · View source @@ -1101,7 +1101,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1125,7 +1125,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1152,7 +1152,7 @@ with 1: Default level: error · Added in 0.0.12 · -Related issues · +Related issues · View source @@ -1189,7 +1189,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · -Related issues · +Related issues · View source @@ -1221,7 +1221,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1250,7 +1250,7 @@ a: str Default level: warn · Added in 0.0.20 · -Related issues · +Related issues · View source @@ -1299,7 +1299,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1343,7 +1343,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · -Related issues · +Related issues · View source @@ -1385,7 +1385,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · -Related issues · +Related issues · View source @@ -1429,7 +1429,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1467,7 +1467,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · -Related issues · +Related issues · View source @@ -1516,7 +1516,7 @@ x: G[int] Default level: warn · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1546,7 +1546,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · -Related issues · +Related issues · View source @@ -1585,7 +1585,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · -Related issues · +Related issues · View source @@ -1646,7 +1646,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1681,7 +1681,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · -Related issues · +Related issues · View source @@ -1709,7 +1709,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1743,7 +1743,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · -Related issues · +Related issues · View source @@ -1850,7 +1850,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · -Related issues · +Related issues · View source @@ -1904,7 +1904,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · -Related issues · +Related issues · View source @@ -1934,7 +1934,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -1984,7 +1984,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2010,7 +2010,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2041,7 +2041,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2075,7 +2075,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2124,7 +2124,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2153,7 +2153,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2199,7 +2199,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)` Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2249,7 +2249,7 @@ class C: ... Default level: error · Added in 0.0.10 · -Related issues · +Related issues · View source @@ -2295,7 +2295,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · -Related issues · +Related issues · View source @@ -2322,7 +2322,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · -Related issues · +Related issues · View source @@ -2369,7 +2369,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2399,7 +2399,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2429,7 +2429,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · -Related issues · +Related issues · View source @@ -2463,7 +2463,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · -Related issues · +Related issues · View source @@ -2497,7 +2497,7 @@ class C: Default level: error · Added in 0.0.15 · -Related issues · +Related issues · View source @@ -2528,7 +2528,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2575,7 +2575,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · -Related issues · +Related issues · View source @@ -2607,7 +2607,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · -Related issues · +Related issues · View source @@ -2642,7 +2642,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · -Related issues · +Related issues · View source @@ -2673,7 +2673,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · -Related issues · +Related issues · View source @@ -2728,7 +2728,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · -Related issues · +Related issues · View source @@ -2771,7 +2771,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2796,7 +2796,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · -Related issues · +Related issues · View source @@ -2829,7 +2829,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2858,7 +2858,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2884,7 +2884,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -2908,7 +2908,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · -Related issues · +Related issues · View source @@ -2941,7 +2941,7 @@ class B(A): Default level: error · Added in 0.0.16 · -Related issues · +Related issues · View source @@ -2974,7 +2974,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3001,7 +3001,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · -Related issues · +Related issues · View source @@ -3028,7 +3028,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · -Related issues · +Related issues · View source @@ -3056,7 +3056,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · -Related issues · +Related issues · View source @@ -3088,7 +3088,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · -Related issues · +Related issues · View source @@ -3125,7 +3125,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3158,7 +3158,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3189,7 +3189,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3216,7 +3216,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · -Related issues · +Related issues · View source @@ -3248,7 +3248,7 @@ class C: Default level: error · Added in 0.0.20 · -Related issues · +Related issues · View source @@ -3282,7 +3282,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3312,7 +3312,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3341,7 +3341,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · -Related issues · +Related issues · View source @@ -3375,7 +3375,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3402,7 +3402,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3430,7 +3430,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3476,7 +3476,7 @@ class A: Default level: error · Added in 0.0.20 · -Related issues · +Related issues · View source @@ -3513,7 +3513,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3537,7 +3537,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3564,7 +3564,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3592,7 +3592,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · -Related issues · +Related issues · View source @@ -3650,7 +3650,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3675,7 +3675,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3700,7 +3700,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · -Related issues · +Related issues · View source @@ -3739,7 +3739,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3776,7 +3776,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · -Related issues · +Related issues · View source @@ -3817,7 +3817,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3845,7 +3845,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: warn · Preview (since 0.0.21) · -Related issues · +Related issues · View source @@ -3878,7 +3878,7 @@ async def main() -> None: Default level: warn · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source @@ -3914,7 +3914,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments. Default level: warn · Added in 0.0.14 · -Related issues · +Related issues · View source @@ -3951,7 +3951,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · -Related issues · +Related issues · View source @@ -4014,7 +4014,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · -Related issues · +Related issues · View source From 1afb169862ac5a416c5b43413f21762651622c9d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Mar 2026 07:22:31 -0500 Subject: [PATCH 203/261] [ty] Add `all` selector to ty.json's `schema` (#23721) Closes https://github.com/astral-sh/ty/issues/2962. --- crates/ty_project/src/metadata/options.rs | 22 +++++++++++++++++++++- ty.schema.json | 9 +++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 10a7d38a4d88e..a5613ceaf2d02 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -1828,7 +1828,7 @@ mod schema { let registry = ty_python_semantic::default_lint_registry(); let level_schema = generator.subschema_for::(); - let properties: Map = registry + let mut properties: Map = registry .lints() .iter() .map(|lint| { @@ -1858,6 +1858,26 @@ mod schema { }) .collect(); + let mut all_schema = schemars::Schema::default(); + let all = all_schema.ensure_object(); + all.insert( + "title".to_string(), + Value::String("set the default severity level for all rules".to_string()), + ); + all.insert( + "description".to_string(), + Value::String( + "Configure a default severity level for all rules. Individual rule settings override this default." + .to_string(), + ), + ); + all.insert( + "oneOf".to_string(), + Value::Array(vec![level_schema.clone().into()]), + ); + + properties.insert("all".to_string(), all_schema.into()); + let mut schema = schemars::json_schema!({ "type": "object" }); let object = schema.ensure_object(); object.insert("properties".to_string(), Value::Object(properties)); diff --git a/ty.schema.json b/ty.schema.json index a95d01a2df0b9..8d25d8dff497d 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -375,6 +375,15 @@ } ] }, + "all": { + "title": "set the default severity level for all rules", + "description": "Configure a default severity level for all rules. Individual rule settings override this default.", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "ambiguous-protocol-member": { "title": "detects protocol classes with ambiguous interfaces", "description": "## What it does\nChecks for protocol classes with members that will lead to ambiguous interfaces.\n\n## Why is this bad?\nAssigning to an undeclared variable in a protocol class leads to an ambiguous\ninterface which may lead to the type checker inferring unexpected things. It's\nrecommended to ensure that all members of a protocol class are explicitly declared.\n\n## Examples\n\n```py\nfrom typing import Protocol\n\nclass BaseProto(Protocol):\n a: int # fine (explicitly declared as `int`)\n def method_member(self) -> int: ... # fine: a method definition using `def` is considered a declaration\n c = \"some variable\" # error: no explicit declaration, leading to ambiguity\n b = method_member # error: no explicit declaration, leading to ambiguity\n\n # error: this creates implicit assignments of `d` and `e` in the protocol class body.\n # Were they really meant to be considered protocol members?\n for d, e in enumerate(range(42)):\n pass\n\nclass SubProto(BaseProto, Protocol):\n a = 42 # fine (declared in superclass)\n```", From da13d62663884af74a82b9ab0e72c1efc6a1aa32 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 5 Mar 2026 12:26:41 +0000 Subject: [PATCH 204/261] [ty] Validate bare ParamSpec usage in type annotations, and support stringified ParamSpecs as the first argument to `Callable` (#23625) --- crates/ty_ide/src/hover.rs | 4 +- .../mdtest/generics/legacy/paramspec.md | 67 ++- .../mdtest/generics/pep695/concatenate.md | 4 +- .../mdtest/generics/pep695/paramspec.md | 52 ++- ...amSpe\342\200\246_(648be2a43987ffd8).snap" | 416 ++++++++++++++++++ ...ramSpe\342\200\246_(327594c6dacd8ad).snap" | 381 ++++++++++++++++ .../mdtest/type_qualifiers/classvar.md | 2 +- crates/ty_python_semantic/src/types.rs | 49 ++- crates/ty_python_semantic/src/types/infer.rs | 26 ++ .../src/types/infer/builder.rs | 44 +- .../infer/builder/annotation_expression.rs | 12 +- .../types/infer/builder/binary_expressions.rs | 3 + .../src/types/infer/builder/subscript.rs | 24 + .../types/infer/builder/type_expression.rs | 120 +++-- .../src/types/known_instance.rs | 7 +- 15 files changed, 1131 insertions(+), 80 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index c36fc71f0fbbb..3feb907dd9aca 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -2519,10 +2519,10 @@ def function(): // TODO: This should just be `**AB@Alias2 ()` // https://github.com/astral-sh/ty/issues/1581 assert_snapshot!(test.hover(), @" - (**AB@Alias2) -> tuple[AB@Alias2] + (**AB@Alias2) -> tuple[Unknown] --------------------------------------------- ```python - (**AB@Alias2) -> tuple[AB@Alias2] + (**AB@Alias2) -> tuple[Unknown] ``` --------------------------------------------- info[hover]: Hovered content is diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index f29db13a19e8a..72aec3936d599 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -153,8 +153,21 @@ class B: ... In type annotations, `ParamSpec` is only valid as the first element to `Callable`, the final element to `Concatenate`, or as a type parameter to `Protocol` or `Generic`. + + +`library.py`: + +```py +from typing import ParamSpec + +LibraryP = ParamSpec("LibraryP") +``` + +`main.py`: + ```py -from typing import ParamSpec, Callable, Concatenate, Protocol, Generic +import library +from typing import Any, Final, ParamSpec, Callable, Concatenate, Protocol, Generic, Union, Optional, Annotated P = ParamSpec("P") @@ -167,19 +180,61 @@ class ValidGeneric(Generic[P]): def valid( a1: Callable[P, int], a2: Callable[Concatenate[int, P], int], + a3: Callable["P", int], + a4: Callable[Concatenate[int, "P"], int], + a5: Callable[library.LibraryP, int], + a6: Callable["Concatenate[int, P]", int], + a7: Callable["library.LibraryP", int], ) -> None: ... def invalid( - # TODO: error + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a1: P, - # TODO: error + # TODO: this should cause us to emit an error because a `ParamSpec` type argument + # cannot be used to specialize a non-`ParamSpec` type parameter a2: list[P], - # TODO: error + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a3: Callable[[P], int], - # TODO: error + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a4: Callable[..., P], - # TODO: error + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a5: Callable[Concatenate[P, ...], int], + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a6: P | int, + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a7: Union[P, int], + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a8: Optional[P], + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a9: Annotated[P, "metadata"], + # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" + a10: Callable["[int, str]", str], + # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" + a11: Callable["...", int], +) -> None: ... + +# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +def invalid_return() -> P: + raise NotImplementedError + +def invalid_variable_annotation(y: Any) -> None: + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + x: P = y + +def invalid_with_qualifier(y: Any) -> None: + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + x: Final[P] = y + +# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +def invalid_stringified_return() -> "P": + raise NotImplementedError + +def invalid_stringified_annotation( + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a: "P", ) -> None: ... +def invalid_stringified_variable_annotation(y: Any) -> None: + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + x: "P" = y ``` ## Validating `P.args` and `P.kwargs` usage diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md index 2fd64d8819c76..2ecc6603cd23f 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md @@ -223,11 +223,11 @@ If a `ParamSpec` appears in `Concatenate`, it must be the last element. ```py from typing import Callable, Concatenate -# TODO: Should be an error - ParamSpec not in last position +# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" def invalid1[**P](c: Callable[Concatenate[P, int], bool]): reveal_type(c) # revealed: (...) -> bool -# TODO: Should be an error - ParamSpec not in last position +# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" def invalid2[**P](c: Callable[Concatenate[P, ...], bool]): reveal_type(c) # revealed: (...) -> bool diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 7b0d89f82e0ad..4c6acdb15776a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -67,25 +67,65 @@ def foo[**P = int]() -> None: `ParamSpec` is only valid as the first element to `Callable` or the final element to `Concatenate`. + + ```py -from typing import ParamSpec, Callable, Concatenate +from typing import Any, Final, ParamSpec, Callable, Concatenate, Union, Optional, Annotated def valid[**P]( a1: Callable[P, int], a2: Callable[Concatenate[int, P], int], + a3: Callable["P", int], + a4: Callable[Concatenate[int, "P"], int], ) -> None: ... def invalid[**P]( - # TODO: error + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a1: P, - # TODO: error + # TODO: this should cause us to emit an error because a `ParamSpec` type argument + # cannot be used to specialize a non-`ParamSpec` type parameter a2: list[P], - # TODO: error + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a3: Callable[[P], int], - # TODO: error + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a4: Callable[..., P], - # TODO: error + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a5: Callable[Concatenate[P, ...], int], + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a6: P | int, + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a7: Union[P, int], + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a8: Optional[P], + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a9: Annotated[P, "metadata"], +) -> None: ... + +# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +def invalid_return[**P]() -> P: + raise NotImplementedError + +# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +type Alias[**P] = P + +def invalid_variable_annotation[**P](y: Any) -> None: + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + x: P = y + +def invalid_with_qualifier[**P](y: Any) -> None: + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + x: Final[P] = y + +# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +def invalid_stringified_return[**P]() -> "P": + raise NotImplementedError + +def invalid_stringified_annotation[**P]( + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + a: "P", ) -> None: ... +def invalid_stringified_variable_annotation[**P](y: Any) -> None: + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + x: "P" = y ``` ## Validating `P.args` and `P.kwargs` usage diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" new file mode 100644 index 0000000000000..0a7cda0a747b5 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" @@ -0,0 +1,416 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: paramspec.md - Legacy `ParamSpec` - Validating `ParamSpec` usage +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +--- + +# Python source files + +## library.py + +``` +1 | from typing import ParamSpec +2 | +3 | LibraryP = ParamSpec("LibraryP") +``` + +## main.py + +``` + 1 | import library + 2 | from typing import Any, Final, ParamSpec, Callable, Concatenate, Protocol, Generic, Union, Optional, Annotated + 3 | + 4 | P = ParamSpec("P") + 5 | + 6 | class ValidProtocol(Protocol[P]): + 7 | def method(self, c: Callable[P, int]) -> None: ... + 8 | + 9 | class ValidGeneric(Generic[P]): +10 | def method(self, c: Callable[P, int]) -> None: ... +11 | +12 | def valid( +13 | a1: Callable[P, int], +14 | a2: Callable[Concatenate[int, P], int], +15 | a3: Callable["P", int], +16 | a4: Callable[Concatenate[int, "P"], int], +17 | a5: Callable[library.LibraryP, int], +18 | a6: Callable["Concatenate[int, P]", int], +19 | a7: Callable["library.LibraryP", int], +20 | ) -> None: ... +21 | def invalid( +22 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +23 | a1: P, +24 | # TODO: this should cause us to emit an error because a `ParamSpec` type argument +25 | # cannot be used to specialize a non-`ParamSpec` type parameter +26 | a2: list[P], +27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +28 | a3: Callable[[P], int], +29 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +30 | a4: Callable[..., P], +31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +32 | a5: Callable[Concatenate[P, ...], int], +33 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +34 | a6: P | int, +35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +36 | a7: Union[P, int], +37 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +38 | a8: Optional[P], +39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +40 | a9: Annotated[P, "metadata"], +41 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +42 | a10: Callable["[int, str]", str], +43 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +44 | a11: Callable["...", int], +45 | ) -> None: ... +46 | +47 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +48 | def invalid_return() -> P: +49 | raise NotImplementedError +50 | +51 | def invalid_variable_annotation(y: Any) -> None: +52 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +53 | x: P = y +54 | +55 | def invalid_with_qualifier(y: Any) -> None: +56 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +57 | x: Final[P] = y +58 | +59 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +60 | def invalid_stringified_return() -> "P": +61 | raise NotImplementedError +62 | +63 | def invalid_stringified_annotation( +64 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +65 | a: "P", +66 | ) -> None: ... +67 | def invalid_stringified_variable_annotation(y: Any) -> None: +68 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +69 | x: "P" = y +``` + +# Diagnostics + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:23:9 + | +21 | def invalid( +22 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +23 | a1: P, + | ^ +24 | # TODO: this should cause us to emit an error because a `ParamSpec` type argument +25 | # cannot be used to specialize a non-`ParamSpec` type parameter + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:28:19 + | +26 | a2: list[P], +27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +28 | a3: Callable[[P], int], + | ^ +29 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +30 | a4: Callable[..., P], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:30:23 + | +28 | a3: Callable[[P], int], +29 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +30 | a4: Callable[..., P], + | ^ +31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +32 | a5: Callable[Concatenate[P, ...], int], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:32:30 + | +30 | a4: Callable[..., P], +31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +32 | a5: Callable[Concatenate[P, ...], int], + | ^ +33 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +34 | a6: P | int, + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:34:9 + | +32 | a5: Callable[Concatenate[P, ...], int], +33 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +34 | a6: P | int, + | ^ +35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +36 | a7: Union[P, int], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:36:15 + | +34 | a6: P | int, +35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +36 | a7: Union[P, int], + | ^ +37 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +38 | a8: Optional[P], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:38:18 + | +36 | a7: Union[P, int], +37 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +38 | a8: Optional[P], + | ^ +39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +40 | a9: Annotated[P, "metadata"], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:40:19 + | +38 | a8: Optional[P], +39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +40 | a9: Annotated[P, "metadata"], + | ^ +41 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +42 | a10: Callable["[int, str]", str], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...` + --> src/main.py:42:19 + | +40 | a9: Annotated[P, "metadata"], +41 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +42 | a10: Callable["[int, str]", str], + | ^^^^^^^^^^^^ +43 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +44 | a11: Callable["...", int], + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...` + --> src/main.py:44:19 + | +42 | a10: Callable["[int, str]", str], +43 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +44 | a11: Callable["...", int], + | ^^^^^ +45 | ) -> None: ... + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:48:25 + | +47 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +48 | def invalid_return() -> P: + | ^ +49 | raise NotImplementedError + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:53:8 + | +51 | def invalid_variable_annotation(y: Any) -> None: +52 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +53 | x: P = y + | ^ +54 | +55 | def invalid_with_qualifier(y: Any) -> None: + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:57:14 + | +55 | def invalid_with_qualifier(y: Any) -> None: +56 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +57 | x: Final[P] = y + | ^ +58 | +59 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:60:38 + | +59 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +60 | def invalid_stringified_return() -> "P": + | ^ +61 | raise NotImplementedError + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:65:9 + | +63 | def invalid_stringified_annotation( +64 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +65 | a: "P", + | ^ +66 | ) -> None: ... +67 | def invalid_stringified_variable_annotation(y: Any) -> None: + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/main.py:69:9 + | +67 | def invalid_stringified_variable_annotation(y: Any) -> None: +68 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +69 | x: "P" = y + | ^ + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" new file mode 100644 index 0000000000000..c5aa764b966ba --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" @@ -0,0 +1,381 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: paramspec.md - PEP 695 `ParamSpec` - Validating `ParamSpec` usage +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Any, Final, ParamSpec, Callable, Concatenate, Union, Optional, Annotated + 2 | + 3 | def valid[**P]( + 4 | a1: Callable[P, int], + 5 | a2: Callable[Concatenate[int, P], int], + 6 | a3: Callable["P", int], + 7 | a4: Callable[Concatenate[int, "P"], int], + 8 | ) -> None: ... + 9 | def invalid[**P]( +10 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +11 | a1: P, +12 | # TODO: this should cause us to emit an error because a `ParamSpec` type argument +13 | # cannot be used to specialize a non-`ParamSpec` type parameter +14 | a2: list[P], +15 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +16 | a3: Callable[[P], int], +17 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +18 | a4: Callable[..., P], +19 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +20 | a5: Callable[Concatenate[P, ...], int], +21 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +22 | a6: P | int, +23 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +24 | a7: Union[P, int], +25 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +26 | a8: Optional[P], +27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +28 | a9: Annotated[P, "metadata"], +29 | ) -> None: ... +30 | +31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +32 | def invalid_return[**P]() -> P: +33 | raise NotImplementedError +34 | +35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +36 | type Alias[**P] = P +37 | +38 | def invalid_variable_annotation[**P](y: Any) -> None: +39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +40 | x: P = y +41 | +42 | def invalid_with_qualifier[**P](y: Any) -> None: +43 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +44 | x: Final[P] = y +45 | +46 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +47 | def invalid_stringified_return[**P]() -> "P": +48 | raise NotImplementedError +49 | +50 | def invalid_stringified_annotation[**P]( +51 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +52 | a: "P", +53 | ) -> None: ... +54 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: +55 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +56 | x: "P" = y +``` + +# Diagnostics + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:11:9 + | + 9 | def invalid[**P]( +10 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +11 | a1: P, + | ^ +12 | # TODO: this should cause us to emit an error because a `ParamSpec` type argument +13 | # cannot be used to specialize a non-`ParamSpec` type parameter + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:16:19 + | +14 | a2: list[P], +15 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +16 | a3: Callable[[P], int], + | ^ +17 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +18 | a4: Callable[..., P], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:18:23 + | +16 | a3: Callable[[P], int], +17 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +18 | a4: Callable[..., P], + | ^ +19 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +20 | a5: Callable[Concatenate[P, ...], int], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:20:30 + | +18 | a4: Callable[..., P], +19 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +20 | a5: Callable[Concatenate[P, ...], int], + | ^ +21 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +22 | a6: P | int, + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:22:9 + | +20 | a5: Callable[Concatenate[P, ...], int], +21 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +22 | a6: P | int, + | ^ +23 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +24 | a7: Union[P, int], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:24:15 + | +22 | a6: P | int, +23 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +24 | a7: Union[P, int], + | ^ +25 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +26 | a8: Optional[P], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:26:18 + | +24 | a7: Union[P, int], +25 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +26 | a8: Optional[P], + | ^ +27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +28 | a9: Annotated[P, "metadata"], + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:28:19 + | +26 | a8: Optional[P], +27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +28 | a9: Annotated[P, "metadata"], + | ^ +29 | ) -> None: ... + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:32:30 + | +31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +32 | def invalid_return[**P]() -> P: + | ^ +33 | raise NotImplementedError + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:36:19 + | +35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +36 | type Alias[**P] = P + | ^ +37 | +38 | def invalid_variable_annotation[**P](y: Any) -> None: + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:40:8 + | +38 | def invalid_variable_annotation[**P](y: Any) -> None: +39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +40 | x: P = y + | ^ +41 | +42 | def invalid_with_qualifier[**P](y: Any) -> None: + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:44:14 + | +42 | def invalid_with_qualifier[**P](y: Any) -> None: +43 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +44 | x: Final[P] = y + | ^ +45 | +46 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:47:43 + | +46 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +47 | def invalid_stringified_return[**P]() -> "P": + | ^ +48 | raise NotImplementedError + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:52:9 + | +50 | def invalid_stringified_annotation[**P]( +51 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +52 | a: "P", + | ^ +53 | ) -> None: ... +54 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression + --> src/mdtest_snippet.py:56:9 + | +54 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: +55 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +56 | x: "P" = y + | ^ + | +info: A bare ParamSpec is only valid: +info: - as the first argument to `Callable` +info: - as the last argument to `Concatenate` +info: - as the default type for another ParamSpec +info: - as part of a type parameter list when defining a generic class +info: - or as part of an argument list when specializing a generic class +info: rule `invalid-type-form` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md index a226c3997ae20..01138809deb43 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -157,7 +157,7 @@ class C(Generic[T, P]): # error: [invalid-type-form] "`ClassVar` cannot contain type variables" c: ClassVar[int | T] - # error: [invalid-type-form] "`ClassVar` cannot contain type variables" + # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" d: ClassVar[P] # No error: no type variables diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 02005655fdba7..cd45f0ca005c2 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -63,6 +63,7 @@ use crate::types::generics::{ ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, }; pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; +use crate::types::infer::InferenceFlags; use crate::types::known_instance::{InternedConstraintSet, InternedType, UnionTypeInstance}; pub use crate::types::method::{BoundMethodType, KnownBoundMethodType, WrapperDescriptorKind}; use crate::types::mro::{MroIterator, StaticMroError}; @@ -77,6 +78,7 @@ use crate::types::special_form::TypeQualifier; use crate::types::tuple::TupleSpec; use crate::types::type_alias::TypeAliasType; pub(crate) use crate::types::typed_dict::TypedDictType; +use crate::types::typevar::TypeVarInstance; pub use crate::types::typevar::{ BindingContext, BoundTypeVarInstance, ParamSpecAttrKind, TypeVarBoundOrConstraints, TypeVarKind, }; @@ -4863,6 +4865,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, scope_id: ScopeId<'db>, typevar_binding_context: Option>, + inference_flags: InferenceFlags, ) -> Result, InvalidTypeExpressionError<'db>> { match self { // Special cases for `float` and `complex` @@ -4907,9 +4910,16 @@ impl<'db> Type<'db> { KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)), KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)), KnownInstanceType::TypeVar(typevar) => { - // TODO: A `ParamSpec` type variable cannot be used in type expressions. This - // requires storing additional context as it's allowed in some places - // (`Concatenate`, `Callable`) but not others. + if !inference_flags.contains(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR) + && typevar.is_paramspec(db) + { + return Err(InvalidTypeExpressionError { + invalid_expressions: smallvec_inline![ + InvalidTypeExpression::InvalidBareParamSpec(*typevar) + ], + fallback_type: Type::unknown(), + }); + } let index = semantic_index(db, scope_id.file(db)); Ok(bind_typevar( db, @@ -4983,7 +4993,12 @@ impl<'db> Type<'db> { let mut builder = UnionBuilder::new(db); let mut invalid_expressions = smallvec::SmallVec::default(); for element in union.elements(db) { - match element.in_type_expression(db, scope_id, typevar_binding_context) { + match element.in_type_expression( + db, + scope_id, + typevar_binding_context, + inference_flags, + ) { Ok(type_expr) => builder = builder.add(type_expr), Err(InvalidTypeExpressionError { fallback_type, @@ -5022,11 +5037,12 @@ impl<'db> Type<'db> { Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")), - Type::TypeAlias(alias) => { - alias - .value_type(db) - .in_type_expression(db, scope_id, typevar_binding_context) - } + Type::TypeAlias(alias) => alias.value_type(db).in_type_expression( + db, + scope_id, + typevar_binding_context, + inference_flags, + ), Type::NewTypeInstance(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec_inline![InvalidTypeExpression::InvalidType( @@ -6542,6 +6558,7 @@ enum InvalidTypeExpression<'db> { TypeQualifierRequiresOneArgument(TypeQualifier), /// Some types are always invalid in type expressions InvalidType(Type<'db>, ScopeId<'db>), + InvalidBareParamSpec(TypeVarInstance<'db>), } impl<'db> InvalidTypeExpression<'db> { @@ -6625,6 +6642,11 @@ impl<'db> InvalidTypeExpression<'db> { "Variable of type `{ty}` is not allowed in a type expression", ty = ty.display(self.db) ), + InvalidTypeExpression::InvalidBareParamSpec(paramspec) => write!( + f, + "Bare ParamSpec `{}` is not valid in this context in a type expression", + paramspec.name(self.db) + ), } } } @@ -6658,7 +6680,7 @@ impl<'db> InvalidTypeExpression<'db> { return; }; if module_member_with_same_name - .in_type_expression(db, scope, None) + .in_type_expression(db, scope, None, InferenceFlags::empty()) .is_err() { return; @@ -6690,6 +6712,13 @@ impl<'db> InvalidTypeExpression<'db> { == builtins_module_scope(db) { diagnostic.set_primary_message("Did you mean `collections.abc.Callable`?"); + } else if matches!(self, InvalidTypeExpression::InvalidBareParamSpec(_)) { + diagnostic.info("A bare ParamSpec is only valid:"); + diagnostic.info(" - as the first argument to `Callable`"); + diagnostic.info(" - as the last argument to `Concatenate`"); + diagnostic.info(" - as the default type for another ParamSpec"); + diagnostic.info(" - as part of a type parameter list when defining a generic class"); + diagnostic.info(" - or as part of an argument list when specializing a generic class"); } } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index c77447049e614..3ea857779e333 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -882,3 +882,29 @@ impl<'db> ExpressionInference<'db> { self.extra.as_ref().and_then(|extra| extra.cycle_recovery) } } + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) struct InferenceFlags: u8 { + /// Whether to allow `ParamSpec` in type expressions. + /// + /// In most contexts inside type expressions, bare `ParamSpec`s are not allowed. + /// They are specifically allowed as the first argument to `Callable`, + /// the second argument to `Concatenate`, and certain other special cases. + const ALLOW_PARAMSPEC_TYPE_EXPR = 1 << 0; + + /// Whether to check for unbound type variables in type expressions. + /// This flag is set when processing annotation expressions, where unbound type variables + /// are an error. It is unset in other contexts (e.g., `TypeVar` defaults, explicit class + /// specialization) where unbound type variables are expected. + const CHECK_UNBOUND_TYPEVARS = 1 << 1; + } +} + +impl InferenceFlags { + fn replace(&mut self, other: Self, set_to: bool) -> bool { + let previously_contained_flag = self.contains(other); + self.set(other, set_to); + previously_contained_flag + } +} diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ac3652f42d199..12305c81b42ef 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -135,14 +135,14 @@ use crate::types::typevar::{ use crate::types::visitor::find_over_type; use crate::types::{ CallDunderError, CallableBinding, CallableType, ClassType, DataclassParams, DynamicType, - EvaluationMode, GenericAlias, InternedConstraintSet, InternedType, IntersectionBuilder, - IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, - LiteralValueTypeKind, MemberLookupPolicy, MetaclassCandidate, ParamSpecAttrKind, Parameter, - ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, - Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, - TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, - UnionType, binding_type, definition_expression_type, infer_complete_scope_types, - infer_scope_types, todo_type, + EvaluationMode, GenericAlias, InferenceFlags, InternedConstraintSet, InternedType, + IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, + LintDiagnosticGuard, LiteralValueTypeKind, MemberLookupPolicy, MetaclassCandidate, + ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, + StaticClassLiteral, SubclassOfType, Truthiness, Type, TypeAliasType, TypeAndQualifiers, + TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, + TypedDictType, UnionBuilder, UnionType, binding_type, definition_expression_type, + infer_complete_scope_types, infer_scope_types, todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; @@ -301,11 +301,9 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { /// Whether we are in a context that binds unbound typevars. typevar_binding_context: Option>, - /// Whether to check for unbound type variables in type expressions. - /// This is set to `true` when processing annotation expressions, where unbound type variables - /// are an error. It is `false` in other contexts (e.g., `TypeVar` defaults, explicit class - /// specialization) where unbound type variables are expected. - check_unbound_typevars: bool, + /// Type-inference is context-dependent, especially in type expressions. + /// This field tracks various flags that control how type inference should behave in the current context. + inference_flags: InferenceFlags, /// The deferred state of inferring types of certain expressions within the region. /// @@ -373,7 +371,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bindings: VecMap::default(), declarations: VecMap::default(), typevar_binding_context: None, - check_unbound_typevars: false, + inference_flags: InferenceFlags::empty(), deferred: VecSet::default(), undecorated_type: None, cycle_recovery: None, @@ -5018,6 +5016,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_paramspec_default(&mut self, default_expr: &ast::Expr) { + let previously_allowed_paramspec = self + .inference_flags + .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true); + self.infer_paramspec_default_impl(default_expr); + self.inference_flags.set( + InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, + previously_allowed_paramspec, + ); + } + + fn infer_paramspec_default_impl(&mut self, default_expr: &ast::Expr) { match default_expr { ast::Expr::EllipsisLiteral(ellipsis) => { let ty = self.infer_ellipsis_literal_expression(ellipsis); @@ -14081,6 +14090,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { db, self.scope(), self.typevar_binding_context, + self.inference_flags ) && !defined_type.member(db, attr_name).place.is_undefined() { diag.help(format_args!( @@ -14673,7 +14683,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // builder only state typevar_binding_context: _, - check_unbound_typevars: _, + inference_flags: _, deferred_state: _, multi_inference_state: _, inner_expression_inference_state: _, @@ -14743,7 +14753,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { dataclass_field_specifiers: _, all_definitely_bound: _, typevar_binding_context: _, - check_unbound_typevars: _, + inference_flags: _, deferred_state: _, inferring_vararg_annotation: _, multi_inference_state: _, @@ -14826,7 +14836,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { dataclass_field_specifiers: _, all_definitely_bound: _, typevar_binding_context: _, - check_unbound_typevars: _, + inference_flags: _, deferred_state: _, multi_inference_state: _, inner_expression_inference_state: _, diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 148778d1272ab..76d8f83ccd297 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -5,6 +5,7 @@ use crate::place::TypeOrigin; use crate::types::diagnostic::{ INVALID_TYPE_FORM, REDUNDANT_FINAL_CLASSVAR, report_invalid_arguments_to_annotated, }; +use crate::types::infer::builder::InferenceFlags; use crate::types::infer::nearest_enclosing_class; use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, @@ -71,10 +72,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state); - let previous_check_unbound_typevars = - std::mem::replace(&mut self.check_unbound_typevars, true); + let previous_check_unbound_typevars = self + .inference_flags + .replace(InferenceFlags::CHECK_UNBOUND_TYPEVARS, true); let annotation_ty = self.infer_annotation_expression_impl(annotation, pep_613_policy); - self.check_unbound_typevars = previous_check_unbound_typevars; + self.inference_flags.set( + InferenceFlags::CHECK_UNBOUND_TYPEVARS, + previous_check_unbound_typevars, + ); self.deferred_state = previous_deferred_state; annotation_ty } @@ -143,6 +148,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.db(), builder.scope(), builder.typevar_binding_context, + builder.inference_flags, ) .unwrap_or_else(|error| { error.into_fallback_type( diff --git a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs index 29d3f7f5c72b0..f56cf80b398fb 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs @@ -78,6 +78,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { [left_ty, right_ty], self.scope(), self.typevar_binding_context, + self.inference_flags, ) } } @@ -703,6 +704,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { [left_ty, right_ty], self.scope(), self.typevar_binding_context, + self.inference_flags, )) } } @@ -729,6 +731,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { [left_ty, right_ty], self.scope(), self.typevar_binding_context, + self.inference_flags, )) } diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index acdc9da74097f..7afa6b335f86a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -17,6 +17,7 @@ use crate::types::diagnostic::{ report_invalid_arguments_to_annotated, }; use crate::types::generics::{GenericContext, InferableTypeVars, bind_typevar}; +use crate::types::infer::InferenceFlags; use crate::types::special_form::AliasSpec; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleType}; @@ -467,6 +468,29 @@ impl<'db> TypeInferenceBuilder<'db, '_> { value_ty: Type<'db>, generic_context: GenericContext<'db>, specialize: &dyn Fn(&[Option>]) -> Type<'db>, + ) -> Type<'db> { + let previously_allowed_paramspec = self + .inference_flags + .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true); + let result = self.infer_explicit_callable_specialization_impl( + subscript, + value_ty, + generic_context, + specialize, + ); + self.inference_flags.set( + InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, + previously_allowed_paramspec, + ); + result + } + + pub(super) fn infer_explicit_callable_specialization_impl( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + generic_context: GenericContext<'db>, + specialize: &dyn Fn(&[Option>]) -> Type<'db>, ) -> Type<'db> { enum ExplicitSpecializationError { InvalidParamSpec, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index f55511381ada3..c62d9a49ab2bc 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -3,13 +3,11 @@ use ruff_python_ast as ast; use super::{DeferredExpressionState, TypeInferenceBuilder}; use crate::FxOrderSet; -use crate::semantic_index::semantic_index; use crate::types::diagnostic::{ self, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, UNBOUND_TYPE_VARIABLE, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable, }; -use crate::types::generics::bind_typevar; -use crate::types::infer::builder::InnerExpressionInferenceState; +use crate::types::infer::builder::{InferenceFlags, InnerExpressionInferenceState}; use crate::types::signatures::Signature; use crate::types::special_form::{AliasSpec, LegacyStdlibAlias}; use crate::types::string_annotation::parse_string_annotation; @@ -86,7 +84,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let ty = self .infer_name_expression(name) .default_specialize(self.db()) - .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) + .in_type_expression( + self.db(), + self.scope(), + self.typevar_binding_context, + self.inference_flags, + ) .unwrap_or_else(|error| { error.into_fallback_type( &self.context, @@ -106,7 +109,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::ExprContext::Load => self .infer_attribute_expression(attribute_expression) .default_specialize(self.db()) - .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) + .in_type_expression( + self.db(), + self.scope(), + self.typevar_binding_context, + self.inference_flags, + ) .unwrap_or_else(|error| { error.into_fallback_type( &self.context, @@ -899,6 +907,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let scope_id = self.scope(); let current_typevar_binding_context = self.typevar_binding_context; + let current_inference_flags = self.inference_flags; // TODO // If we explicitly specialize a recursive generic (PEP-613 or implicit) type alias, @@ -922,7 +931,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); return if in_type_expression { value_ty - .in_type_expression(db, scope_id, current_typevar_binding_context) + .in_type_expression( + db, + scope_id, + current_typevar_binding_context, + current_inference_flags, + ) .unwrap_or_else(|_| Type::unknown()) } else { value_ty @@ -937,7 +951,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if in_type_expression { specialized - .in_type_expression(db, scope_id, current_typevar_binding_context) + .in_type_expression( + db, + scope_id, + current_typevar_binding_context, + current_inference_flags, + ) .unwrap_or_else(|_| Type::unknown()) } else { specialized @@ -1068,6 +1087,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.db(), self.scope(), self.typevar_binding_context, + self.inference_flags, ) .unwrap_or(Type::unknown()) } @@ -1200,6 +1220,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.db(), self.scope(), self.typevar_binding_context, + self.inference_flags, ) .unwrap_or(Type::unknown()) } @@ -1376,10 +1397,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // in the global scope or similar should be considered to create an implicit generic context. // For now, we do not report unbound type variables in any `Callable` contexts, but we may // decide to revisit this in the future. - let previous_check_unbound_typevars = - std::mem::replace(&mut self.check_unbound_typevars, false); + let previous_check_unbound_typevars = self + .inference_flags + .replace(InferenceFlags::CHECK_UNBOUND_TYPEVARS, false); let result = inner(self, subscript); - self.check_unbound_typevars = previous_check_unbound_typevars; + self.inference_flags.set( + InferenceFlags::CHECK_UNBOUND_TYPEVARS, + previous_check_unbound_typevars, + ); result } @@ -1397,7 +1422,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::SpecialForm(SpecialFormType::Annotated), subscript, ) - .in_type_expression(db, self.scope(), None) + .in_type_expression(db, self.scope(), None, self.inference_flags) .unwrap_or_else(|err| err.into_fallback_type(&self.context, subscript, true)); // Only store on the tuple slice; non-tuple cases are handled by // `infer_subscript_load_impl` via `infer_expression`. @@ -1678,15 +1703,26 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { std::slice::from_ref(arguments_slice) }; - for argument in arguments { + + for (i, argument) in arguments.iter().enumerate() { if argument.is_ellipsis_literal_expr() { // The trailing `...` in `Concatenate[int, str, ...]` is valid; // store without going through type-expression inference. self.store_expression_type(argument, Type::unknown()); + } else if i < arguments.len() - 1 { + self.infer_type_expression(argument); } else { + let previously_allowed_paramspec = self + .inference_flags + .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true); self.infer_type_expression(argument); + self.inference_flags.set( + InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, + previously_allowed_paramspec, + ); } } + let num_arguments = arguments.len(); let inferred_type = if num_arguments < 2 { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -1974,29 +2010,50 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // TODO: Support `Concatenate[...]` return Some(Parameters::todo()); } - ast::Expr::Name(name) => { - if name.is_invalid() { + ast::Expr::Name(_) | ast::Expr::Attribute(_) => { + if parameters + .as_name_expr() + .is_some_and(ast::ExprName::is_invalid) + { // This is a special case to avoid raising the error suggesting what the first // argument should be. This only happens when there's already a syntax error like // `Callable[]`. return None; } - let name_ty = self.infer_name_load(name); - if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = name_ty - && typevar.is_paramspec(self.db()) + let previously_allowed_paramspec = self + .inference_flags + .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true); + let parameters_type = self.infer_type_expression_no_store(parameters); + self.inference_flags.set( + InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, + previously_allowed_paramspec, + ); + if let Type::TypeVar(tvar) = parameters_type + && tvar.is_paramspec(self.db()) { - let index = semantic_index(self.db(), self.scope().file(self.db())); - let Some(bound_typevar) = bind_typevar( - self.db(), - index, - self.scope().file_scope_id(self.db()), - self.typevar_binding_context, - typevar, - ) else { - // TODO: What to do here? - return None; - }; - return Some(Parameters::paramspec(self.db(), bound_typevar)); + return Some(Parameters::paramspec(self.db(), tvar)); + } + } + ast::Expr::StringLiteral(string) => { + if let Some(parsed) = parse_string_annotation(&self.context, string) { + self.string_annotations + .insert(ruff_python_ast::ExprRef::StringLiteral(string).into()); + let node_key = self.enclosing_node_key(string.into()); + + let previous_deferred_state = std::mem::replace( + &mut self.deferred_state, + DeferredExpressionState::InStringAnnotation(node_key), + ); + let result = matches!( + parsed.expr(), + ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) + ) + .then(|| self.infer_callable_parameter_types(parsed.expr())); + self.deferred_state = previous_deferred_state; + + if let Some(result) = result { + return result; + } } } _ => {} @@ -2020,7 +2077,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { expression: &ast::Expr, ty: Type<'db>, ) -> Type<'db> { - if !self.check_unbound_typevars { + if !self + .inference_flags + .contains(InferenceFlags::CHECK_UNBOUND_TYPEVARS) + { return ty; } if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty { diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 736c72bcc7e56..b73740880a6c6 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -5,8 +5,8 @@ use crate::{ semantic_index::{definition::Definition, scope::ScopeId}, types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassType, GenericContext, - InvalidTypeExpressionError, KnownClass, StringLiteralType, Type, TypeAliasType, - TypeContext, TypeMapping, TypeVarVariance, UnionBuilder, + InferenceFlags, InvalidTypeExpressionError, KnownClass, StringLiteralType, Type, + TypeAliasType, TypeContext, TypeMapping, TypeVarVariance, UnionBuilder, class::NamedTupleSpec, constraints::OwnedConstraintSet, generics::{Specialization, walk_generic_context}, @@ -426,10 +426,11 @@ impl<'db> UnionTypeInstance<'db> { value_expr_types: [Type<'db>; 2], scope_id: ScopeId<'db>, typevar_binding_context: Option>, + inference_flags: InferenceFlags, ) -> Type<'db> { let mut builder = UnionBuilder::new(db); for ty in &value_expr_types { - match ty.in_type_expression(db, scope_id, typevar_binding_context) { + match ty.in_type_expression(db, scope_id, typevar_binding_context, inference_flags) { Ok(ty) => builder.add_in_place(ty), Err(error) => { return Type::KnownInstance(KnownInstanceType::UnionType( From 5f0fd91a230972bb9d1e4545ebaed2b7d09158a2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 5 Mar 2026 13:06:54 +0000 Subject: [PATCH 205/261] [ty] More type-variable default validation (#23639) ## Summary We have several checks at the moment which fire on legacy type variables with invalid defaults in a _class_ context, but which fail to check for legacy type variables with invalid defaults in a _function_ context. This PR adds those missing checks. The typing spec states that both legacy and PEP-695 type variables are not allowed to have defaults that: - reference type variables bound in outer scopes, or - reference type variables that are put into scope later on in the type parameter list or function signature The typing spec also states that a type variable with a default is not allowed to come before any type variables without defaults. ## Test Plan mdtests and snapshots --- .../resources/mdtest/generics/scoping.md | 135 ++++- ..._in_c\342\200\246_(1a50b4ccb10b95dd).snap" | 39 ++ ...in_me\342\200\246_(2ed4c18a38ed9090).snap" | 46 ++ ...in_ne\342\200\246_(a1aca17ea750ffdd).snap" | 49 ++ ...order\342\200\246_(d075a45828c9dbc5).snap" | 120 +++++ ...with_\342\200\246_(ce8defbeaf54e06c).snap" | 48 ++ ..._Nested_functions_(3f2ee9fa81da0177).snap" | 38 ++ ...ed_in\342\200\246_(de027dcc5360f252).snap" | 41 ++ .../src/types/infer/builder.rs | 474 +++++++++++++++--- .../ty_python_semantic/src/types/typevar.rs | 17 +- 10 files changed, 942 insertions(+), 65 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index 03e007681916d..16acbce4ded4c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -5,8 +5,7 @@ python-version = "3.12" ``` -Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing -spec. +Most of these tests come from the [Scoping rules for type variables] section of the typing spec. ## Typevar used outside of generic function or class @@ -410,6 +409,135 @@ class C[T]: ok2: Inner[T] ``` +## Type parameter defaults cannot reference outer-scope type parameters + +```toml +[environment] +python-version = "3.13" +``` + +Per the [typing spec][scoping rules], the default of a type parameter must not reference type +parameters from an outer scope. Out-of-scope defaults on class type parameters are validated as part +of `invalid-generic-class`; the tests here cover the remaining cases for PEP 695 function and type +alias scopes, as well as legacy `TypeVar`s used in function/method signatures. + +### Nested functions + + + +```py +def outer[T](): + # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" + def inner[U = T](): ... + def ok[U = int](): ... # OK +``` + +### Function nested in class + + + +```py +class C[T]: + # error: [invalid-type-variable-default] + def f[U = T](self): ... + def g[U = int](self): ... # OK +``` + +### Type alias nested in class + + + +```py +class C[T]: + # error: [invalid-type-variable-default] + type Alias[U = T] = list[U] + + type Ok[U = int] = list[U] # OK +``` + +### Legacy TypeVar in method with outer-scope class TypeVar + + + +```py +from typing import TypeVar, Generic + +T1 = TypeVar("T1") +T2 = TypeVar("T2", default=T1) + +class Foo(Generic[T1]): + # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `T1`" + def method(self, x: T2) -> T2: + return x +``` + +### Legacy TypeVar in nested function + + + +```py +from typing import TypeVar, Generic + +T = TypeVar("T") +U = TypeVar("U", default=T) + +def outer(x: T) -> T: + # error: [invalid-type-variable-default] + def inner(y: U) -> U: + return y + return x +``` + +### Legacy TypeVar with default referring to later Typevar + + + +```py +from typing import TypeVar, Generic + +T = TypeVar("T", default=int) +U = TypeVar("U", default=T) + +# error: [invalid-type-variable-default] +def bad(y: U, z: T) -> tuple[U, T]: + return y, z + +# OK, because the typevar with the default comes after the one without +def fine(y: T, z: U) -> tuple[U, T]: + return z, y +``` + +### Legacy TypeVar ordering: default before non-default in function + + + +```py +from typing import TypeVar + +T1 = TypeVar("T1", default=int) +T2 = TypeVar("T2") +T3 = TypeVar("T3") +DefaultStrT = TypeVar("DefaultStrT", default=str) + +# error: [invalid-type-variable-default] +def f(x: T1, y: T2) -> tuple[T1, T2]: + return x, y + +# error: [invalid-type-variable-default] +def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: + return x, y, z + +# error: [invalid-type-variable-default] +def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: + return x, y, z, w + +def ok(x: T2, y: T1) -> tuple[T2, T1]: + return x, y + +def ok2(x: T1, y: DefaultStrT) -> tuple[T1, DefaultStrT]: + return x, y +``` + ## Mixed-scope type parameters Methods can have type parameters that are scoped to the method itself, while also referring to type @@ -433,4 +561,5 @@ def f(x: type[Foo[T]]) -> T: raise NotImplementedError ``` -[scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables +[scoping rules]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules +[scoping rules for type variables]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" new file mode 100644 index 0000000000000..bf806c00c1af7 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" @@ -0,0 +1,39 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 624 +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Function nested in class +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C[T]: +2 | # error: [invalid-type-variable-default] +3 | def f[U = T](self): ... +4 | def g[U = int](self): ... # OK +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid default for type parameter `U` + --> src/mdtest_snippet.py:1:9 + | +1 | class C[T]: + | - `T` defined here +2 | # error: [invalid-type-variable-default] +3 | def f[U = T](self): ... + | ^ `T` is a type parameter bound in an outer scope +4 | def g[U = int](self): ... # OK + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" new file mode 100644 index 0000000000000..ba4ce7ed1fb27 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" @@ -0,0 +1,46 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar in method with outer-scope class TypeVar +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar, Generic +2 | +3 | T1 = TypeVar("T1") +4 | T2 = TypeVar("T2", default=T1) +5 | +6 | class Foo(Generic[T1]): +7 | # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `T1`" +8 | def method(self, x: T2) -> T2: +9 | return x +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid use of type variable `T2` + --> src/mdtest_snippet.py:4:1 + | +3 | T1 = TypeVar("T1") +4 | T2 = TypeVar("T2", default=T1) + | ------------------------------ `T2` defined here +5 | +6 | class Foo(Generic[T1]): +7 | # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable … +8 | def method(self, x: T2) -> T2: + | ^^ Default of `T2` references out-of-scope type variable `T1` +9 | return x + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" new file mode 100644 index 0000000000000..cf21e648fa8cd --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" @@ -0,0 +1,49 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 624 +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar in nested function +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar, Generic + 2 | + 3 | T = TypeVar("T") + 4 | U = TypeVar("U", default=T) + 5 | + 6 | def outer(x: T) -> T: + 7 | # error: [invalid-type-variable-default] + 8 | def inner(y: U) -> U: + 9 | return y +10 | return x +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid use of type variable `U` + --> src/mdtest_snippet.py:4:1 + | + 3 | T = TypeVar("T") + 4 | U = TypeVar("U", default=T) + | --------------------------- `U` defined here + 5 | + 6 | def outer(x: T) -> T: + 7 | # error: [invalid-type-variable-default] + 8 | def inner(y: U) -> U: + | ^ Default of `U` references out-of-scope type variable `T` + 9 | return y +10 | return x + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" new file mode 100644 index 0000000000000..dd6d4aab02275 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" @@ -0,0 +1,120 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar ordering: default before non-default in function +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar + 2 | + 3 | T1 = TypeVar("T1", default=int) + 4 | T2 = TypeVar("T2") + 5 | T3 = TypeVar("T3") + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + 7 | + 8 | # error: [invalid-type-variable-default] + 9 | def f(x: T1, y: T2) -> tuple[T1, T2]: +10 | return x, y +11 | +12 | # error: [invalid-type-variable-default] +13 | def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: +14 | return x, y, z +15 | +16 | # error: [invalid-type-variable-default] +17 | def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: +18 | return x, y, z, w +19 | +20 | def ok(x: T2, y: T1) -> tuple[T2, T1]: +21 | return x, y +22 | +23 | def ok2(x: T1, y: DefaultStrT) -> tuple[T1, DefaultStrT]: +24 | return x, y +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:9:10 + | + 8 | # error: [invalid-type-variable-default] + 9 | def f(x: T1, y: T2) -> tuple[T1, T2]: + | -- ^^ Type variable `T2` does not have a default + | | + | Earlier TypeVar `T1` has a default +10 | return x, y + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | T2 = TypeVar("T2") + | ------------------ `T2` defined here + 5 | T3 = TypeVar("T3") + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + | +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:13:17 + | +12 | # error: [invalid-type-variable-default] +13 | def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: + | -- ^^ Type variable `T3` does not have a default + | | + | Earlier TypeVar `T1` has a default +14 | return x, y, z + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | T2 = TypeVar("T2") + 5 | T3 = TypeVar("T3") + | ------------------ `T3` defined here + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + | +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:17:10 + | +16 | # error: [invalid-type-variable-default] +17 | def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: + | -- ^^ Type variables `T2` and `T3` do not have defaults + | | + | Earlier TypeVar `T1` has a default +18 | return x, y, z, w + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | T2 = TypeVar("T2") + | ------------------ `T2` defined here + 5 | T3 = TypeVar("T3") + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + | +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" new file mode 100644 index 0000000000000..87d020644136f --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" @@ -0,0 +1,48 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar with default referring to later Typevar +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar, Generic + 2 | + 3 | T = TypeVar("T", default=int) + 4 | U = TypeVar("U", default=T) + 5 | + 6 | # error: [invalid-type-variable-default] + 7 | def bad(y: U, z: T) -> tuple[U, T]: + 8 | return y, z + 9 | +10 | # OK, because the typevar with the default comes after the one without +11 | def fine(y: T, z: U) -> tuple[U, T]: +12 | return z, y +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid use of type variable `U` + --> src/mdtest_snippet.py:4:1 + | +3 | T = TypeVar("T", default=int) +4 | U = TypeVar("U", default=T) + | --------------------------- `U` defined here +5 | +6 | # error: [invalid-type-variable-default] +7 | def bad(y: U, z: T) -> tuple[U, T]: + | ^ Default of `U` references later type parameter `T` +8 | return y, z + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" new file mode 100644 index 0000000000000..418057f234a37 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" @@ -0,0 +1,38 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Nested functions +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def outer[T](): +2 | # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" +3 | def inner[U = T](): ... +4 | def ok[U = int](): ... # OK +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid default for type parameter `U` + --> src/mdtest_snippet.py:1:11 + | +1 | def outer[T](): + | - `T` defined here +2 | # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" +3 | def inner[U = T](): ... + | ^ `T` is a type parameter bound in an outer scope +4 | def ok[U = int](): ... # OK + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" new file mode 100644 index 0000000000000..3ebc3bab85c36 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 624 +expression: snapshot +--- + +--- +mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Type alias nested in class +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C[T]: +2 | # error: [invalid-type-variable-default] +3 | type Alias[U = T] = list[U] +4 | +5 | type Ok[U = int] = list[U] # OK +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Invalid default for type parameter `U` + --> src/mdtest_snippet.py:1:9 + | +1 | class C[T]: + | - `T` defined here +2 | # error: [invalid-type-variable-default] +3 | type Alias[U = T] = list[U] + | ^ `T` is a type parameter bound in an outer scope +4 | +5 | type Ok[U = int] = list[U] # OK + | +info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 12305c81b42ef..862f68851e913 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -59,6 +59,7 @@ use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, attribute_assignments, place_table, }; +use crate::types::BindingContext; use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::callable::CallableTypeKind; @@ -689,21 +690,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.check_dynamic_class_definitions(&deferred_definitions); self.check_overloaded_functions(node); self.check_type_guard_definitions(); - self.check_legacy_positional_only_convention(); + self.check_function_definitions(); self.check_final_without_value(); } } - /// Iterate over all function definitions in this scope and check for invalid applications - /// of the pre-PEP-570 positional-only parameter convention. - fn check_legacy_positional_only_convention(&mut self) { + /// Iterate over all function definitions in this scope and run checks that + /// require access to the function's inferred signature (and therefore must + /// run after deferred inference is complete). + fn check_function_definitions(&self) { let db = self.db(); for (definition, _) in &self.declarations { if !definition.kind(db).is_function_def() { continue; } - let Some(Type::FunctionLiteral(function_type)) = infer_definition_types(db, *definition).undecorated_type() else { @@ -711,56 +712,313 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let last_definition = function_type.literal(db).last_definition(db); - let node = last_definition.node(db, self.file(), self.module()); + let signature = last_definition.raw_signature(db); - let ast_parameters = &node.parameters; - // If the function has any PEP-570 positional-only parameters, - // assume that `__`-prefixed parameters are not meant to be positional-only - if !ast_parameters.posonlyargs.is_empty() { + self.check_legacy_positional_only_convention(last_definition, &signature); + self.check_legacy_typevar_defaults(last_definition, &signature); + self.check_legacy_typevar_ordering(last_definition, &signature); + } + } + + /// Check for invalid applications of the pre-PEP-570 positional-only parameter convention. + fn check_legacy_positional_only_convention( + &self, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, + ) { + let node = last_definition.node(self.db(), self.file(), self.module()); + let ast_parameters = &node.parameters; + + // If the function has any PEP-570 positional-only parameters, + // assume that `__`-prefixed parameters are not meant to be positional-only + if !ast_parameters.posonlyargs.is_empty() { + return; + } + let parsed_parameters = signature.parameters(); + let mut previous_non_positional_only: Option<&ast::ParameterWithDefault> = None; + + for (param_node, param) in std::iter::zip(ast_parameters, parsed_parameters) { + let AnyParameterRef::NonVariadic(param_node) = param_node else { + continue; + }; + if param.is_positional_only() { continue; } - let signature = last_definition.raw_signature(db); - let parsed_parameters = signature.parameters(); - let mut previous_non_positional_only: Option<&ast::ParameterWithDefault> = None; - for (param_node, param) in std::iter::zip(ast_parameters, parsed_parameters) { - let AnyParameterRef::NonVariadic(param_node) = param_node else { + // Valid uses of the PEP-484 positional-only convention will have been detected as such + // in the first iteration over this scope, so `param.is_positional_only()` will return `true` + // for those. We only get here for invalid uses of the PEP-484 positional-only convention. + if param_node.uses_pep_484_positional_only_convention() { + let Some(builder) = self + .context + .report_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER, param_node.name()) + else { continue; }; - if param.is_positional_only() { - continue; + let mut diagnostic = builder.into_diagnostic( + "Invalid use of the legacy convention \ + for positional-only parameters", + ); + diagnostic.set_primary_message( + "Parameter name begins with `__` but will not be treated as positional-only", + ); + diagnostic.info( + "A parameter can only be positional-only \ + if it precedes all positional-or-keyword parameters", + ); + if let Some(earlier_node) = previous_non_positional_only { + diagnostic.annotate( + self.context + .secondary(earlier_node.name()) + .message("Prior parameter here was positional-or-keyword"), + ); } + } else if previous_non_positional_only.is_none() { + previous_non_positional_only = Some(param_node); + } + } + } - if param_node.uses_pep_484_positional_only_convention() { - if let Some(builder) = self - .context - .report_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER, param_node.name()) - { - let mut diagnostic = builder.into_diagnostic( - "Invalid use of the legacy convention \ - for positional-only parameters", - ); - diagnostic.set_primary_message( - "Parameter name begins with `__` \ - but will not be treated as positional-only", - ); - diagnostic.info( - "A parameter can only be positional-only \ - if it precedes all positional-or-keyword parameters", - ); - if let Some(earlier_node) = previous_non_positional_only { - diagnostic.annotate( - self.context - .secondary(earlier_node.name()) - .message("Prior parameter here was positional-or-keyword"), - ); - } - } - } else if previous_non_positional_only.is_none() { - previous_non_positional_only = Some(param_node); + /// Find the range of the first parameter annotation (or return type) in a function + /// whose inferred type references the given `TypeVar`, falling back to the function name. + fn find_typevar_annotation_range( + &self, + node: &ast::StmtFunctionDef, + typevar: TypeVarInstance<'db>, + ) -> TextRange { + let db = self.db(); + let typevar_id = typevar.identity(self.db()); + + node.parameters + .iter() + .filter_map(ast::AnyParameterRef::annotation) + .chain(node.returns.as_deref()) + .find(|ann| { + self.file_expression_type(ann) + .references_typevar(db, typevar_id) + }) + .map(Ranged::range) + .unwrap_or(node.name.range()) + } + + /// Check whether any legacy `TypeVar` used in a function signature has a default + /// that references an out-of-scope type variable. + /// + /// This check mirrors the class-level check at `report_invalid_typevar_default_reference`, + /// but for function/method generic contexts. + fn check_legacy_typevar_defaults( + &self, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, + ) { + let db = self.db(); + + let Some(generic_context) = signature.generic_context else { + return; + }; + + let typevars = generic_context + .variables(db) + .map(|bound_tvar| bound_tvar.typevar(db)); + + for (i, typevar) in typevars.clone().enumerate() { + // Only check legacy TypeVars; PEP 695 type parameters are already validated + // by `check_default_for_outer_scope_typevars` in the type parameter scope. + if !matches!( + typevar.kind(db), + TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec + ) { + continue; + } + + let Some(default_ty) = typevar.default_type(db) else { + continue; + }; + + let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let tvar = match t { + Type::TypeVar(tvar) => tvar.typevar(db), + Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, + _ => return None, + }; + if !typevars.clone().take(i).contains(&tvar) { + Some(tvar) + } else { + None + } + }); + + let Some(bad_typevar) = first_bad_tvar else { + continue; + }; + + let is_later_in_list = typevars.clone().skip(i).contains(&bad_typevar); + let node = last_definition.node(db, self.file(), self.module()); + + let primary_range = self.find_typevar_annotation_range(node, typevar); + + let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, primary_range) + else { + continue; + }; + let typevar_name = typevar.name(db); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid use of type variable `{typevar_name}`", + )); + + if is_later_in_list { + diagnostic.set_primary_message(format_args!( + "Default of `{typevar_name}` references later type parameter `{}`", + bad_typevar.name(db), + )); + diagnostic.set_concise_message(format_args!( + "Invalid use of type variable `{typevar_name}`: default of `{typevar_name}` \ + refers to later parameter `{}`", + bad_typevar.name(db) + )); + } else { + diagnostic.set_primary_message(format_args!( + "Default of `{typevar_name}` references out-of-scope type variable `{}`", + bad_typevar.name(db), + )); + diagnostic.set_concise_message(format_args!( + "Invalid use of type variable `{typevar_name}`: default of `{typevar_name}` \ + refers to out-of-scope type variable `{}`", + bad_typevar.name(db) + )); + } + + if let Some(typevar_definition) = typevar.definition(db) { + let file = typevar_definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + typevar_definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{typevar_name}` defined here")), + ); + } + + diagnostic + .info("See https://typing.python.org/en/latest/spec/generics.html#scoping-rules"); + } + } + + /// Check that legacy `TypeVar`s without defaults don't follow `TypeVar`s with defaults + /// in a function's generic context. + /// + /// This mirrors the class-level check using `report_invalid_type_param_order`, but for + /// function/method generic contexts using the `invalid-type-variable-default` lint. + fn check_legacy_typevar_ordering( + &self, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, + ) { + struct State<'db> { + typevar_with_default: TypeVarInstance<'db>, + invalid_later_tvars: Vec>, + } + + let db = self.db(); + + let Some(generic_context) = signature.generic_context else { + return; + }; + + let mut state: Option> = None; + + for bound_typevar in generic_context.variables(db) { + let typevar = bound_typevar.typevar(db); + + // Only check legacy TypeVars; PEP 695 ordering is validated by the parser. + if !matches!( + typevar.kind(db), + TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec + ) { + continue; + } + + let has_default = typevar.default_type(db).is_some(); + + if let Some(state) = state.as_mut() { + if !has_default { + state.invalid_later_tvars.push(typevar); } + } else if has_default { + state = Some(State { + typevar_with_default: typevar, + invalid_later_tvars: vec![], + }); } } + + let Some(state) = state else { + return; + }; + + if state.invalid_later_tvars.is_empty() { + return; + } + + let node = last_definition.node(db, self.file(), self.module()); + + let primary_range = self.find_typevar_annotation_range(node, state.invalid_later_tvars[0]); + + let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, primary_range) + else { + return; + }; + + let mut diagnostic = builder.into_diagnostic( + "Type parameters without defaults cannot follow type parameters with defaults", + ); + + let typevar_with_default_name = state.typevar_with_default.name(db); + + diagnostic.set_concise_message(format_args!( + "Type parameter `{}` without a default cannot follow \ + earlier parameter `{typevar_with_default_name}` with a default", + state.invalid_later_tvars[0].name(db), + )); + + if let [single_typevar] = &*state.invalid_later_tvars { + diagnostic.set_primary_message(format_args!( + "Type variable `{}` does not have a default", + single_typevar.name(db), + )); + } else { + let later_typevars = + format_enumeration(state.invalid_later_tvars.iter().map(|tv| tv.name(db))); + diagnostic.set_primary_message(format_args!( + "Type variables {later_typevars} do not have defaults", + )); + } + + let secondary_range = self.find_typevar_annotation_range(node, state.typevar_with_default); + + diagnostic.annotate( + self.context + .secondary(secondary_range) + .message(format_args!( + "Earlier TypeVar `{typevar_with_default_name}` has a default" + )), + ); + + for tvar in [state.typevar_with_default, state.invalid_later_tvars[0]] { + let Some(definition) = tvar.definition(db) else { + continue; + }; + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{}` defined here", tvar.name(db))), + ); + } } /// Iterate over all static class definitions (created using `class` statements) to check that @@ -4657,17 +4915,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(default_expr) = default.as_deref() { let default_ty = self.infer_type_expression(default_expr); - let bound_node = bound_node.map(|n| match n { - ast::Expr::Tuple(tuple) => BoundOrConstraintsNodes::Constraints(&tuple.elts), - _ => BoundOrConstraintsNodes::Bound(n), - }); - self.validate_typevar_default( - Some(&name.id), - bound_or_constraints, - default_ty, - default_expr, - bound_node, - ); + if !self.check_default_for_outer_scope_typevars(default_ty, default_expr, &name.id) { + let bound_node = bound_node.map(|n| match n { + ast::Expr::Tuple(tuple) => BoundOrConstraintsNodes::Constraints(&tuple.elts), + _ => BoundOrConstraintsNodes::Bound(n), + }); + self.validate_typevar_default( + Some(&name.id), + bound_or_constraints, + default_ty, + default_expr, + bound_node, + ); + } } self.deferred_state = previous_deferred_state; } @@ -4965,6 +5225,82 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Check if a PEP 695 type parameter's default references type variables from an outer scope. + /// + /// Returns `true` if such a reference was found and a diagnostic was emitted, + /// indicating that further default validation should be skipped. + /// + /// Note: this only handles PEP 695 type parameters in function and type alias scopes. + /// Class type parameter scopes are skipped here because out-of-scope references + /// are validated at the class level via `report_invalid_typevar_default_reference`. + /// Legacy `TypeVar`s are validated by `check_legacy_typevar_defaults`. + fn check_default_for_outer_scope_typevars( + &self, + default_ty: Type<'db>, + default_node: &ast::Expr, + typevar_name: &str, + ) -> bool { + let db = self.db(); + + // Determine the expected binding context from the current type parameter scope. + // Only check function and type alias scopes; class scopes are handled separately + // when processing the class definition. + let expected_binding_def = match self.scope().node(db) { + NodeWithScopeKind::FunctionTypeParameters(function) => { + self.index.expect_single_definition(function) + } + NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => { + self.index.expect_single_definition(type_alias) + } + _ => return false, + }; + let expected_binding = BindingContext::Definition(expected_binding_def); + + let outer_tv = find_over_type(db, default_ty, false, |ty| { + if let Type::TypeVar(bound_tv) = ty + && bound_tv.binding_context(db) != expected_binding + { + Some(bound_tv) + } else { + None + } + }); + + let Some(outer_tv) = outer_tv else { + return false; + }; + let outer_typevar = outer_tv.typevar(db); + let outer_name = outer_typevar.name(db); + let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_node) + else { + return false; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid default for type parameter `{typevar_name}`" + )); + diagnostic.set_primary_message(format_args!( + "`{outer_name}` is a type parameter bound in an outer scope" + )); + diagnostic.set_concise_message(format_args!( + "Type parameter `{typevar_name}` cannot use \ + outer-scope type parameter `{outer_name}` as its default" + )); + if let Some(definition) = outer_typevar.definition(db) { + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{outer_name}` defined here")), + ); + } + diagnostic.info("See https://typing.python.org/en/latest/spec/generics.html#scoping-rules"); + + true + } + fn infer_paramspec_definition( &mut self, node: &ast::TypeParamParamSpec, @@ -5003,7 +5339,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let ast::TypeParamParamSpec { range: _, node_index: _, - name: _, + name, default: Some(default), } = node else { @@ -5011,22 +5347,26 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let previous_deferred_state = std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred); - self.infer_paramspec_default(default); + self.infer_paramspec_default(default, Some(&name.id)); self.deferred_state = previous_deferred_state; } - fn infer_paramspec_default(&mut self, default_expr: &ast::Expr) { + fn infer_paramspec_default(&mut self, default_expr: &ast::Expr, paramspec_name: Option<&str>) { let previously_allowed_paramspec = self .inference_flags .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true); - self.infer_paramspec_default_impl(default_expr); + self.infer_paramspec_default_impl(default_expr, paramspec_name); self.inference_flags.set( InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, previously_allowed_paramspec, ); } - fn infer_paramspec_default_impl(&mut self, default_expr: &ast::Expr) { + fn infer_paramspec_default_impl( + &mut self, + default_expr: &ast::Expr, + paramspec_name: Option<&str>, + ) { match default_expr { ast::Expr::EllipsisLiteral(ellipsis) => { let ty = self.infer_ellipsis_literal_expression(ellipsis); @@ -5048,6 +5388,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } ast::Expr::Name(_) => { let ty = self.infer_type_expression(default_expr); + if let Some(name) = paramspec_name + && self.check_default_for_outer_scope_typevars(ty, default_expr, name) + { + return; + } let is_paramspec = match ty { Type::TypeVar(typevar) => typevar.is_paramspec(self.db()), Type::KnownInstance(known_instance) => { @@ -7617,7 +7962,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { known_class, Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) ) { - self.infer_paramspec_default(&default.value); + // Pass `None` for the name: the outer-scope typevar check inside + // `infer_paramspec_default` is only relevant for PEP 695 type parameter + // scopes. Legacy ParamSpec definitions live at module/class-body scope, + // so the check would be a no-op here. Out-of-scope defaults for legacy + // typevars are instead validated by `check_legacy_typevar_defaults` + // (for functions) and `report_invalid_typevar_default_reference` + // (for classes). + self.infer_paramspec_default(&default.value, None); } else { let default_ty = self.infer_type_expression(&default.value); let bound_or_constraints_node = arguments diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 7dfe530bed89d..74b20481105df 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -35,6 +35,20 @@ impl<'db> Type<'db> { any_over_type(db, self, false, |ty| matches!(ty, Type::TypeVar(_))) } + pub(crate) fn references_typevar( + self, + db: &'db dyn Db, + typevar_id: TypeVarIdentity<'db>, + ) -> bool { + any_over_type(db, self, false, |ty| match ty { + Type::TypeVar(bound_typevar) => typevar_id == bound_typevar.typevar(db).identity(db), + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { + typevar_id == typevar.identity(db) + } + _ => false, + }) + } + pub(crate) fn has_non_self_typevar(self, db: &'db dyn Db) -> bool { any_over_type( db, @@ -593,7 +607,8 @@ impl<'db> TypeVarInstance<'db> { ) -> Option> { let default = self.lazy_default_unchecked(db)?; - // Unlike bounds/constraints, default types are allowed to be generic (https://peps.python.org/pep-0696/#using-another-type-parameter-as-default). + // Unlike bounds/constraints, default types are allowed to be generic + // (https://typing.python.org/en/latest/spec/generics.html#defaults-for-type-parameters). // Here we simply check for non-self-referential. // TODO: We should also check for non-forward references. if self.type_is_self_referential(db, default, visitor) { From db2849068f7d6a1f42cdafec46a7c2c83d39ece3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Mar 2026 08:39:48 -0500 Subject: [PATCH 206/261] [ty] Override home directory in ty tests (#23724) Closes https://github.com/astral-sh/ty/issues/2942. --- crates/ty/tests/cli/main.rs | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs index a741f4ef29897..a9e735fd5b788 100644 --- a/crates/ty/tests/cli/main.rs +++ b/crates/ty/tests/cli/main.rs @@ -383,12 +383,8 @@ fn user_configuration() -> anyhow::Result<()> { ), ])?; - let config_directory = case.root().join("home/.config"); - let config_env_var = if cfg!(windows) { - "APPDATA" - } else { - "XDG_CONFIG_HOME" - }; + let config_directory = case.user_config_directory(); + let config_env_var = user_config_directory_env_var(); assert_cmd_snapshot!( case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), @@ -888,13 +884,16 @@ impl CliTest { // Canonicalize the tempdir path because macos uses symlinks for tempdirs // and that doesn't play well with our snapshot filtering. // Simplify with dunce because otherwise we get UNC paths on Windows. - let project_dir = dunce::simplified( + let temp_dir_path = dunce::simplified( &temp_dir .path() .canonicalize() - .context("Failed to canonicalize project path")?, + .context("Failed to canonicalize temporary directory path")?, ) .to_path_buf(); + let project_dir = temp_dir_path.join("project"); + std::fs::create_dir_all(&project_dir) + .with_context(|| format!("Failed to create directory `{}`", project_dir.display()))?; let mut settings = insta::Settings::clone_current(); settings.add_filter(&tempdir_filter(&project_dir), "/"); @@ -1016,9 +1015,21 @@ impl CliTest { // Unset all environment variables because they can affect test behavior. command.env_clear(); + // Point user config discovery at a test-local directory to avoid picking up host config. + command.env( + user_config_directory_env_var(), + self.user_config_directory(), + ); command } + + fn user_config_directory(&self) -> PathBuf { + self.project_dir + .parent() + .expect("project directory always has a parent") + .join("home/.config") + } } fn tempdir_filter(path: &Path) -> String { @@ -1032,3 +1043,11 @@ fn site_packages_filter(python_version: &str) -> String { format!("lib/python{}/site-packages", regex::escape(python_version)) } } + +fn user_config_directory_env_var() -> &'static str { + if cfg!(windows) { + "APPDATA" + } else { + "XDG_CONFIG_HOME" + } +} From db77d7b2ae3da8deed64d8889a5cbcea287b52a6 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 5 Mar 2026 14:51:54 +0000 Subject: [PATCH 207/261] [ty] Add a diagnostic if a `TypeVar` is used to specialize a `ParamSpec`, or vice versa (#23738) --- .../mdtest/generics/legacy/paramspec.md | 38 ++- .../mdtest/generics/pep695/paramspec.md | 41 ++- ...amSpe\342\200\246_(648be2a43987ffd8).snap" | 259 +++++++++--------- ...not_s\342\200\246_(c9dbdc7b13b704a4).snap" | 82 ++++++ ...ramSpe\342\200\246_(327594c6dacd8ad).snap" | 241 ++++++++-------- ...not_s\342\200\246_(8243f67799c93e3c).snap" | 112 ++++++++ .../src/types/infer/builder/subscript.rs | 40 ++- 7 files changed, 553 insertions(+), 260 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 72aec3936d599..867d77da3bd15 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -189,9 +189,6 @@ def valid( def invalid( # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a1: P, - # TODO: this should cause us to emit an error because a `ParamSpec` type argument - # cannot be used to specialize a non-`ParamSpec` type parameter - a2: list[P], # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a3: Callable[[P], int], # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" @@ -437,6 +434,41 @@ both mypy and Pyright allow this and there are usages of this in the wild e.g., reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int ``` +## `ParamSpec` cannot specialize a `TypeVar`, and vice versa + + + +A `ParamSpec` is not a valid type argument for a regular `TypeVar`, and vice versa. + +```py +from typing import Generic, Callable, TypeVar, ParamSpec + +T = TypeVar("T") +P = ParamSpec("P") + +class OnlyTypeVar(Generic[T]): + attr: T + +def func(c: Callable[P, None]): + # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" + a: OnlyTypeVar[P] + +class OnlyParamSpec(Generic[P]): + attr: Callable[P, None] + +# This is fine due to the special case whereby `OnlyParamSpec[T]` is interpreted the same as +# `OnlyParamSpec[[T]]`, due to the fact that `OnlyParamSpec` is only generic over a single +# `ParamSpec` and no other type variables. +def func2(c: OnlyParamSpec[T], other: T): + reveal_type(c.attr) # revealed: (T@func2, /) -> None + +class ParamSpecAndTypeVar(Generic[P, T]): + attr: Callable[P, T] + +# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +def func3(c: ParamSpecAndTypeVar[T, int], other: T): ... +``` + ## Specialization when defaults are involved ```toml diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 4c6acdb15776a..a97eb8bf3ecd3 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -81,9 +81,6 @@ def valid[**P]( def invalid[**P]( # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a1: P, - # TODO: this should cause us to emit an error because a `ParamSpec` type argument - # cannot be used to specialize a non-`ParamSpec` type parameter - a2: list[P], # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" a3: Callable[[P], int], # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" @@ -379,6 +376,44 @@ both mypy and Pyright allow this and there are usages of this in the wild e.g., reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int ``` +## `ParamSpec` cannot specialize a `TypeVar`, and vice versa + + + +A `ParamSpec` is not a valid type argument for a regular `TypeVar`, and vice versa. + +```py +from typing import Callable + +class OnlyTypeVar[T]: + attr: T + +class TypeVarAndParamSpec[T, **P]: + attr: Callable[P, T] + +def f[**P, T](): + # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" + a: OnlyTypeVar[P] + + # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" + b: TypeVarAndParamSpec[P, [int]] + +class OnlyParamSpec[**P]: + attr: Callable[P, None] + +# This is fine due to the special case whereby `OnlyParamSpec[T]` is interpreted the same as +# `OnlyParamSpec[[T]]`, due to the fact that `OnlyParamSpec` is only generic over a single +# `ParamSpec` and no other type variables. +def func2[T](c: OnlyParamSpec[T], other: T): + reveal_type(c.attr) # revealed: (T@func2, /) -> None + +class ParamSpecAndTypeVar[**P, T]: + attr: Callable[P, T] + +# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +def func3[T](c: ParamSpecAndTypeVar[T, int], other: T): ... +``` + ## Specialization when defaults are involved ```py diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" index 0a7cda0a747b5..3ff6516f4a927 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" @@ -44,52 +44,49 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspe 21 | def invalid( 22 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" 23 | a1: P, -24 | # TODO: this should cause us to emit an error because a `ParamSpec` type argument -25 | # cannot be used to specialize a non-`ParamSpec` type parameter -26 | a2: list[P], -27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -28 | a3: Callable[[P], int], -29 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -30 | a4: Callable[..., P], -31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -32 | a5: Callable[Concatenate[P, ...], int], -33 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -34 | a6: P | int, -35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -36 | a7: Union[P, int], -37 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -38 | a8: Optional[P], -39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -40 | a9: Annotated[P, "metadata"], -41 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" -42 | a10: Callable["[int, str]", str], -43 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" -44 | a11: Callable["...", int], -45 | ) -> None: ... -46 | -47 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -48 | def invalid_return() -> P: -49 | raise NotImplementedError -50 | -51 | def invalid_variable_annotation(y: Any) -> None: -52 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -53 | x: P = y -54 | -55 | def invalid_with_qualifier(y: Any) -> None: -56 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -57 | x: Final[P] = y -58 | -59 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -60 | def invalid_stringified_return() -> "P": -61 | raise NotImplementedError -62 | -63 | def invalid_stringified_annotation( -64 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -65 | a: "P", -66 | ) -> None: ... -67 | def invalid_stringified_variable_annotation(y: Any) -> None: -68 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -69 | x: "P" = y +24 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +25 | a3: Callable[[P], int], +26 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +27 | a4: Callable[..., P], +28 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +29 | a5: Callable[Concatenate[P, ...], int], +30 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +31 | a6: P | int, +32 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +33 | a7: Union[P, int], +34 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +35 | a8: Optional[P], +36 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +37 | a9: Annotated[P, "metadata"], +38 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +39 | a10: Callable["[int, str]", str], +40 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +41 | a11: Callable["...", int], +42 | ) -> None: ... +43 | +44 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +45 | def invalid_return() -> P: +46 | raise NotImplementedError +47 | +48 | def invalid_variable_annotation(y: Any) -> None: +49 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +50 | x: P = y +51 | +52 | def invalid_with_qualifier(y: Any) -> None: +53 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +54 | x: Final[P] = y +55 | +56 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +57 | def invalid_stringified_return() -> "P": +58 | raise NotImplementedError +59 | +60 | def invalid_stringified_annotation( +61 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +62 | a: "P", +63 | ) -> None: ... +64 | def invalid_stringified_variable_annotation(y: Any) -> None: +65 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +66 | x: "P" = y ``` # Diagnostics @@ -102,8 +99,8 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t 22 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" 23 | a1: P, | ^ -24 | # TODO: this should cause us to emit an error because a `ParamSpec` type argument -25 | # cannot be used to specialize a non-`ParamSpec` type parameter +24 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +25 | a3: Callable[[P], int], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -117,14 +114,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:28:19 + --> src/main.py:25:19 | -26 | a2: list[P], -27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -28 | a3: Callable[[P], int], +23 | a1: P, +24 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +25 | a3: Callable[[P], int], | ^ -29 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -30 | a4: Callable[..., P], +26 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +27 | a4: Callable[..., P], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -138,14 +135,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:30:23 + --> src/main.py:27:23 | -28 | a3: Callable[[P], int], -29 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -30 | a4: Callable[..., P], +25 | a3: Callable[[P], int], +26 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +27 | a4: Callable[..., P], | ^ -31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -32 | a5: Callable[Concatenate[P, ...], int], +28 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +29 | a5: Callable[Concatenate[P, ...], int], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -159,14 +156,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:32:30 + --> src/main.py:29:30 | -30 | a4: Callable[..., P], -31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -32 | a5: Callable[Concatenate[P, ...], int], +27 | a4: Callable[..., P], +28 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +29 | a5: Callable[Concatenate[P, ...], int], | ^ -33 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -34 | a6: P | int, +30 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +31 | a6: P | int, | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -180,14 +177,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:34:9 + --> src/main.py:31:9 | -32 | a5: Callable[Concatenate[P, ...], int], -33 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -34 | a6: P | int, +29 | a5: Callable[Concatenate[P, ...], int], +30 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +31 | a6: P | int, | ^ -35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -36 | a7: Union[P, int], +32 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +33 | a7: Union[P, int], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -201,14 +198,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:36:15 + --> src/main.py:33:15 | -34 | a6: P | int, -35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -36 | a7: Union[P, int], +31 | a6: P | int, +32 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +33 | a7: Union[P, int], | ^ -37 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -38 | a8: Optional[P], +34 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +35 | a8: Optional[P], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -222,14 +219,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:38:18 + --> src/main.py:35:18 | -36 | a7: Union[P, int], -37 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -38 | a8: Optional[P], +33 | a7: Union[P, int], +34 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +35 | a8: Optional[P], | ^ -39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -40 | a9: Annotated[P, "metadata"], +36 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +37 | a9: Annotated[P, "metadata"], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -243,14 +240,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:40:19 + --> src/main.py:37:19 | -38 | a8: Optional[P], -39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -40 | a9: Annotated[P, "metadata"], +35 | a8: Optional[P], +36 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +37 | a9: Annotated[P, "metadata"], | ^ -41 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" -42 | a10: Callable["[int, str]", str], +38 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +39 | a10: Callable["[int, str]", str], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -264,14 +261,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...` - --> src/main.py:42:19 + --> src/main.py:39:19 | -40 | a9: Annotated[P, "metadata"], -41 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" -42 | a10: Callable["[int, str]", str], +37 | a9: Annotated[P, "metadata"], +38 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +39 | a10: Callable["[int, str]", str], | ^^^^^^^^^^^^ -43 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" -44 | a11: Callable["...", int], +40 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +41 | a11: Callable["...", int], | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -281,13 +278,13 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...` - --> src/main.py:44:19 + --> src/main.py:41:19 | -42 | a10: Callable["[int, str]", str], -43 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" -44 | a11: Callable["...", int], +39 | a10: Callable["[int, str]", str], +40 | # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +41 | a11: Callable["...", int], | ^^^^^ -45 | ) -> None: ... +42 | ) -> None: ... | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -297,12 +294,12 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:48:25 + --> src/main.py:45:25 | -47 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -48 | def invalid_return() -> P: +44 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +45 | def invalid_return() -> P: | ^ -49 | raise NotImplementedError +46 | raise NotImplementedError | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -316,14 +313,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:53:8 + --> src/main.py:50:8 | -51 | def invalid_variable_annotation(y: Any) -> None: -52 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -53 | x: P = y +48 | def invalid_variable_annotation(y: Any) -> None: +49 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +50 | x: P = y | ^ -54 | -55 | def invalid_with_qualifier(y: Any) -> None: +51 | +52 | def invalid_with_qualifier(y: Any) -> None: | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -337,14 +334,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:57:14 + --> src/main.py:54:14 | -55 | def invalid_with_qualifier(y: Any) -> None: -56 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -57 | x: Final[P] = y +52 | def invalid_with_qualifier(y: Any) -> None: +53 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +54 | x: Final[P] = y | ^ -58 | -59 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +55 | +56 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -358,12 +355,12 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:60:38 + --> src/main.py:57:38 | -59 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -60 | def invalid_stringified_return() -> "P": +56 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +57 | def invalid_stringified_return() -> "P": | ^ -61 | raise NotImplementedError +58 | raise NotImplementedError | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -377,14 +374,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:65:9 + --> src/main.py:62:9 | -63 | def invalid_stringified_annotation( -64 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -65 | a: "P", +60 | def invalid_stringified_annotation( +61 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +62 | a: "P", | ^ -66 | ) -> None: ... -67 | def invalid_stringified_variable_annotation(y: Any) -> None: +63 | ) -> None: ... +64 | def invalid_stringified_variable_annotation(y: Any) -> None: | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -398,11 +395,11 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/main.py:69:9 + --> src/main.py:66:9 | -67 | def invalid_stringified_variable_annotation(y: Any) -> None: -68 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -69 | x: "P" = y +64 | def invalid_stringified_variable_annotation(y: Any) -> None: +65 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +66 | x: "P" = y | ^ | info: A bare ParamSpec is only valid: diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" new file mode 100644 index 0000000000000..81ce304ed4947 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" @@ -0,0 +1,82 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: paramspec.md - Legacy `ParamSpec` - `ParamSpec` cannot specialize a `TypeVar`, and vice versa +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Generic, Callable, TypeVar, ParamSpec + 2 | + 3 | T = TypeVar("T") + 4 | P = ParamSpec("P") + 5 | + 6 | class OnlyTypeVar(Generic[T]): + 7 | attr: T + 8 | + 9 | def func(c: Callable[P, None]): +10 | # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" +11 | a: OnlyTypeVar[P] +12 | +13 | class OnlyParamSpec(Generic[P]): +14 | attr: Callable[P, None] +15 | +16 | # This is fine due to the special case whereby `OnlyParamSpec[T]` is interpreted the same as +17 | # `OnlyParamSpec[[T]]`, due to the fact that `OnlyParamSpec` is only generic over a single +18 | # `ParamSpec` and no other type variables. +19 | def func2(c: OnlyParamSpec[T], other: T): +20 | reveal_type(c.attr) # revealed: (T@func2, /) -> None +21 | +22 | class ParamSpecAndTypeVar(Generic[P, T]): +23 | attr: Callable[P, T] +24 | +25 | # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +26 | def func3(c: ParamSpecAndTypeVar[T, int], other: T): ... +``` + +# Diagnostics + +``` +error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type variable `T` + --> src/mdtest_snippet.py:11:20 + | + 9 | def func(c: Callable[P, None]): +10 | # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" +11 | a: OnlyTypeVar[P] + | ^ +12 | +13 | class OnlyParamSpec(Generic[P]): + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import Generic, Callable, TypeVar, ParamSpec + 2 | + 3 | T = TypeVar("T") + | - Type variable `T` defined here + 4 | P = ParamSpec("P") + | - ParamSpec `P` defined here + 5 | + 6 | class OnlyTypeVar(Generic[T]): + | +info: rule `invalid-type-arguments` is enabled by default + +``` + +``` +error[invalid-type-arguments]: Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...` + --> src/mdtest_snippet.py:26:34 + | +25 | # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +26 | def func3(c: ParamSpecAndTypeVar[T, int], other: T): ... + | ^ + | +info: rule `invalid-type-arguments` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" index c5aa764b966ba..69a918e09fcde 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" @@ -24,51 +24,48 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspe 9 | def invalid[**P]( 10 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" 11 | a1: P, -12 | # TODO: this should cause us to emit an error because a `ParamSpec` type argument -13 | # cannot be used to specialize a non-`ParamSpec` type parameter -14 | a2: list[P], -15 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -16 | a3: Callable[[P], int], -17 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -18 | a4: Callable[..., P], -19 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -20 | a5: Callable[Concatenate[P, ...], int], -21 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -22 | a6: P | int, -23 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -24 | a7: Union[P, int], -25 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -26 | a8: Optional[P], -27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -28 | a9: Annotated[P, "metadata"], -29 | ) -> None: ... -30 | -31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -32 | def invalid_return[**P]() -> P: -33 | raise NotImplementedError +12 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +13 | a3: Callable[[P], int], +14 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +15 | a4: Callable[..., P], +16 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +17 | a5: Callable[Concatenate[P, ...], int], +18 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +19 | a6: P | int, +20 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +21 | a7: Union[P, int], +22 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +23 | a8: Optional[P], +24 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +25 | a9: Annotated[P, "metadata"], +26 | ) -> None: ... +27 | +28 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +29 | def invalid_return[**P]() -> P: +30 | raise NotImplementedError +31 | +32 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +33 | type Alias[**P] = P 34 | -35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -36 | type Alias[**P] = P -37 | -38 | def invalid_variable_annotation[**P](y: Any) -> None: -39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -40 | x: P = y -41 | -42 | def invalid_with_qualifier[**P](y: Any) -> None: -43 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -44 | x: Final[P] = y -45 | -46 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -47 | def invalid_stringified_return[**P]() -> "P": -48 | raise NotImplementedError -49 | -50 | def invalid_stringified_annotation[**P]( -51 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -52 | a: "P", -53 | ) -> None: ... -54 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: -55 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -56 | x: "P" = y +35 | def invalid_variable_annotation[**P](y: Any) -> None: +36 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +37 | x: P = y +38 | +39 | def invalid_with_qualifier[**P](y: Any) -> None: +40 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +41 | x: Final[P] = y +42 | +43 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +44 | def invalid_stringified_return[**P]() -> "P": +45 | raise NotImplementedError +46 | +47 | def invalid_stringified_annotation[**P]( +48 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +49 | a: "P", +50 | ) -> None: ... +51 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: +52 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +53 | x: "P" = y ``` # Diagnostics @@ -81,8 +78,8 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t 10 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" 11 | a1: P, | ^ -12 | # TODO: this should cause us to emit an error because a `ParamSpec` type argument -13 | # cannot be used to specialize a non-`ParamSpec` type parameter +12 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +13 | a3: Callable[[P], int], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -96,14 +93,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:16:19 + --> src/mdtest_snippet.py:13:19 | -14 | a2: list[P], -15 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -16 | a3: Callable[[P], int], +11 | a1: P, +12 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +13 | a3: Callable[[P], int], | ^ -17 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -18 | a4: Callable[..., P], +14 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +15 | a4: Callable[..., P], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -117,14 +114,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:18:23 + --> src/mdtest_snippet.py:15:23 | -16 | a3: Callable[[P], int], -17 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -18 | a4: Callable[..., P], +13 | a3: Callable[[P], int], +14 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +15 | a4: Callable[..., P], | ^ -19 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -20 | a5: Callable[Concatenate[P, ...], int], +16 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +17 | a5: Callable[Concatenate[P, ...], int], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -138,14 +135,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:20:30 + --> src/mdtest_snippet.py:17:30 | -18 | a4: Callable[..., P], -19 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -20 | a5: Callable[Concatenate[P, ...], int], +15 | a4: Callable[..., P], +16 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +17 | a5: Callable[Concatenate[P, ...], int], | ^ -21 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -22 | a6: P | int, +18 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +19 | a6: P | int, | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -159,14 +156,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:22:9 + --> src/mdtest_snippet.py:19:9 | -20 | a5: Callable[Concatenate[P, ...], int], -21 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -22 | a6: P | int, +17 | a5: Callable[Concatenate[P, ...], int], +18 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +19 | a6: P | int, | ^ -23 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -24 | a7: Union[P, int], +20 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +21 | a7: Union[P, int], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -180,14 +177,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:24:15 + --> src/mdtest_snippet.py:21:15 | -22 | a6: P | int, -23 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -24 | a7: Union[P, int], +19 | a6: P | int, +20 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +21 | a7: Union[P, int], | ^ -25 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -26 | a8: Optional[P], +22 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +23 | a8: Optional[P], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -201,14 +198,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:26:18 + --> src/mdtest_snippet.py:23:18 | -24 | a7: Union[P, int], -25 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -26 | a8: Optional[P], +21 | a7: Union[P, int], +22 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +23 | a8: Optional[P], | ^ -27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -28 | a9: Annotated[P, "metadata"], +24 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +25 | a9: Annotated[P, "metadata"], | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -222,13 +219,13 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:28:19 + --> src/mdtest_snippet.py:25:19 | -26 | a8: Optional[P], -27 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -28 | a9: Annotated[P, "metadata"], +23 | a8: Optional[P], +24 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +25 | a9: Annotated[P, "metadata"], | ^ -29 | ) -> None: ... +26 | ) -> None: ... | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -242,12 +239,12 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:32:30 + --> src/mdtest_snippet.py:29:30 | -31 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -32 | def invalid_return[**P]() -> P: +28 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +29 | def invalid_return[**P]() -> P: | ^ -33 | raise NotImplementedError +30 | raise NotImplementedError | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -261,13 +258,13 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:36:19 + --> src/mdtest_snippet.py:33:19 | -35 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -36 | type Alias[**P] = P +32 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +33 | type Alias[**P] = P | ^ -37 | -38 | def invalid_variable_annotation[**P](y: Any) -> None: +34 | +35 | def invalid_variable_annotation[**P](y: Any) -> None: | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -281,14 +278,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:40:8 + --> src/mdtest_snippet.py:37:8 | -38 | def invalid_variable_annotation[**P](y: Any) -> None: -39 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -40 | x: P = y +35 | def invalid_variable_annotation[**P](y: Any) -> None: +36 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +37 | x: P = y | ^ -41 | -42 | def invalid_with_qualifier[**P](y: Any) -> None: +38 | +39 | def invalid_with_qualifier[**P](y: Any) -> None: | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -302,14 +299,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:44:14 + --> src/mdtest_snippet.py:41:14 | -42 | def invalid_with_qualifier[**P](y: Any) -> None: -43 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -44 | x: Final[P] = y +39 | def invalid_with_qualifier[**P](y: Any) -> None: +40 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +41 | x: Final[P] = y | ^ -45 | -46 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +42 | +43 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -323,12 +320,12 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:47:43 + --> src/mdtest_snippet.py:44:43 | -46 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -47 | def invalid_stringified_return[**P]() -> "P": +43 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +44 | def invalid_stringified_return[**P]() -> "P": | ^ -48 | raise NotImplementedError +45 | raise NotImplementedError | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -342,14 +339,14 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:52:9 + --> src/mdtest_snippet.py:49:9 | -50 | def invalid_stringified_annotation[**P]( -51 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -52 | a: "P", +47 | def invalid_stringified_annotation[**P]( +48 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +49 | a: "P", | ^ -53 | ) -> None: ... -54 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: +50 | ) -> None: ... +51 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` @@ -363,11 +360,11 @@ info: rule `invalid-type-form` is enabled by default ``` error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type expression - --> src/mdtest_snippet.py:56:9 + --> src/mdtest_snippet.py:53:9 | -54 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: -55 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" -56 | x: "P" = y +51 | def invalid_stringified_variable_annotation[**P](y: Any) -> None: +52 | # error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context" +53 | x: "P" = y | ^ | info: A bare ParamSpec is only valid: diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" new file mode 100644 index 0000000000000..0a8dbe9ef34be --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" @@ -0,0 +1,112 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: paramspec.md - PEP 695 `ParamSpec` - `ParamSpec` cannot specialize a `TypeVar`, and vice versa +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Callable + 2 | + 3 | class OnlyTypeVar[T]: + 4 | attr: T + 5 | + 6 | class TypeVarAndParamSpec[T, **P]: + 7 | attr: Callable[P, T] + 8 | + 9 | def f[**P, T](): +10 | # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" +11 | a: OnlyTypeVar[P] +12 | +13 | # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" +14 | b: TypeVarAndParamSpec[P, [int]] +15 | +16 | class OnlyParamSpec[**P]: +17 | attr: Callable[P, None] +18 | +19 | # This is fine due to the special case whereby `OnlyParamSpec[T]` is interpreted the same as +20 | # `OnlyParamSpec[[T]]`, due to the fact that `OnlyParamSpec` is only generic over a single +21 | # `ParamSpec` and no other type variables. +22 | def func2[T](c: OnlyParamSpec[T], other: T): +23 | reveal_type(c.attr) # revealed: (T@func2, /) -> None +24 | +25 | class ParamSpecAndTypeVar[**P, T]: +26 | attr: Callable[P, T] +27 | +28 | # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +29 | def func3[T](c: ParamSpecAndTypeVar[T, int], other: T): ... +``` + +# Diagnostics + +``` +error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type variable `T` + --> src/mdtest_snippet.py:9:9 + | + 7 | attr: Callable[P, T] + 8 | + 9 | def f[**P, T](): + | - ParamSpec `P` defined here +10 | # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" +11 | a: OnlyTypeVar[P] + | ^ +12 | +13 | # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" + | + ::: src/mdtest_snippet.py:3:19 + | + 1 | from typing import Callable + 2 | + 3 | class OnlyTypeVar[T]: + | - Type variable `T` defined here + 4 | attr: T + | +info: rule `invalid-type-arguments` is enabled by default + +``` + +``` +error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type variable `T` + --> src/mdtest_snippet.py:14:28 + | +13 | # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" +14 | b: TypeVarAndParamSpec[P, [int]] + | ^ +15 | +16 | class OnlyParamSpec[**P]: + | + ::: src/mdtest_snippet.py:6:27 + | + 4 | attr: T + 5 | + 6 | class TypeVarAndParamSpec[T, **P]: + | - Type variable `T` defined here + 7 | attr: Callable[P, T] + 8 | + 9 | def f[**P, T](): + | - ParamSpec `P` defined here +10 | # error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`" +11 | a: OnlyTypeVar[P] + | +info: rule `invalid-type-arguments` is enabled by default + +``` + +``` +error[invalid-type-arguments]: Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...` + --> src/mdtest_snippet.py:29:37 + | +28 | # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +29 | def func3[T](c: ParamSpecAndTypeVar[T, int], other: T): ... + | ^ + | +info: rule `invalid-type-arguments` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 7afa6b335f86a..d4ba2ac0084a7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -494,6 +494,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) -> Type<'db> { enum ExplicitSpecializationError { InvalidParamSpec, + ParamSpecForTypeVar, UnsatisfiedBound, UnsatisfiedConstraints, /// These two errors override the errors above, causing all specializations to be `Unknown`. @@ -580,6 +581,42 @@ impl<'db> TypeInferenceBuilder<'db, '_> { inferred_type_arguments.push(provided_type); + // A ParamSpec cannot be used to specialize a regular TypeVar. + if !typevar.is_paramspec(db) + && let Type::TypeVar(tv) = provided_type + && tv.is_paramspec(db) + { + let node = get_node(index); + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "ParamSpec `{}` cannot be used to specialize \ + type variable `{}`", + tv.typevar(db).name(db), + typevar.name(db), + )); + for (kind, var) in [("ParamSpec", tv), ("Type variable", typevar)] { + let Some(definition) = var.typevar(db).definition(db) else { + continue; + }; + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let range = definition.focus_range(db, &module).range(); + diagnostic.annotate( + Annotation::secondary(Span::from(file).with_range(range)) + .message(format_args!( + "{kind} `{}` defined here", + var.name(db) + )), + ); + } + } + error = Some(ExplicitSpecializationError::ParamSpecForTypeVar); + specialization_types.push(Some(Type::unknown())); + continue; + } + // TODO consider just accepting the given specialization without checking // against bounds/constraints, but recording the expression for deferred // checking at end of scope. This would avoid a lot of cycles caused by eagerly @@ -779,7 +816,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Some( ExplicitSpecializationError::UnsatisfiedBound | ExplicitSpecializationError::UnsatisfiedConstraints - | ExplicitSpecializationError::InvalidParamSpec, + | ExplicitSpecializationError::InvalidParamSpec + | ExplicitSpecializationError::ParamSpecForTypeVar, ) | None => specialize(&specialization_types), } From 2a5384b0b6e22ab511aec6f8dbb11648befda887 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 5 Mar 2026 16:20:48 +0100 Subject: [PATCH 208/261] [ty] Make `all` selector case sensitive (#23713) --- crates/ty/tests/cli/rule_selection.rs | 29 ----------------------- crates/ty_project/src/metadata/options.rs | 2 +- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs index 1e1eb4ad8a110..a0be66d18279c 100644 --- a/crates/ty/tests/cli/rule_selection.rs +++ b/crates/ty/tests/cli/rule_selection.rs @@ -1013,35 +1013,6 @@ fn cli_all_rules_with_override() -> anyhow::Result<()> { Ok(()) } -/// The "all" keyword is case-insensitive -#[test] -fn cli_all_rules_case_insensitive() -> anyhow::Result<()> { - let case = CliTest::with_file( - "test.py", - r#" - prin(x) # unresolved-reference - "#, - )?; - - // Using --ignore ALL (uppercase) should work the same as --ignore all - assert_cmd_snapshot!( - case - .command() - .arg("--ignore") - .arg("ALL"), - @" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - " - ); - - Ok(()) -} - /// A specific rule can be set first and then overridden by "all" #[test] fn cli_specific_then_all() -> anyhow::Result<()> { diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index a5613ceaf2d02..862fdebb84287 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -927,7 +927,7 @@ impl Rules { }; // Handle "all" as a special case - apply the level to all rules - if rule_name.eq_ignore_ascii_case("all") { + if rule_name.as_str() == "all" { for lint in registry.lints() { set_lint_level(*lint); } From a6a5e8d10b8a5185049827be0a304db522b91c9a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Mar 2026 10:28:24 -0500 Subject: [PATCH 209/261] [ty] Fix precedence of `all` selector in TOML configurations (#23723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Closes https://github.com/astral-sh/ty/issues/2952. TOML tables are unordered — the spec says key/value pairs within tables are not guaranteed to be in any specific order. However, the `toml` crate happens to sort keys lexicographically when deserializing. This means that rules like `abstract-method-in-final-class` sort *before* `all`, so the `all` selector silently overrides the more-specific rule when processed sequentially. This PR fixes the issue by sorting selectors from the same configuration file so that `all` is always applied first, then specific per-rule selectors are applied afterwards. This ensures that specific rules always take precedence over `all` within a given config file, regardless of lexicographic ordering. The fix is scoped to file-based configuration only — CLI argument order is still preserved as-is, since users have explicit control over ordering there. ## Test plan Added a test (`configuration_all_rules_with_rule_sorting_before_all`) that uses `abstract-method-in-final-class` (which sorts before `all`) to verify the specific rule takes precedence. --- crates/ty/tests/cli/rule_selection.rs | 126 ++++++++++++++++++++ crates/ty_project/src/metadata/options.rs | 35 +++++- crates/ty_project/src/metadata/pyproject.rs | 11 +- 3 files changed, 170 insertions(+), 2 deletions(-) diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs index a0be66d18279c..4f1af154c1e2b 100644 --- a/crates/ty/tests/cli/rule_selection.rs +++ b/crates/ty/tests/cli/rule_selection.rs @@ -1092,3 +1092,129 @@ fn configuration_all_rules() -> anyhow::Result<()> { Ok(()) } + +/// In TOML, key order in a table is not semantically meaningful, so specific rules should +/// still override `all` even if they sort lexicographically before `all`. +#[test] +fn configuration_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + all = "warn" + abstract-method-in-final-class = "error" + "#, + ), + ( + "test.py", + r#" + from typing import final + from abc import ABC, abstractmethod + + class Base(ABC): + @abstractmethod + def foo(self) -> int: + raise NotImplementedError + + @final + class Derived(Base): + pass + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @" + success: false + exit_code: 1 + ----- stdout ----- + error[abstract-method-in-final-class]: Final class `Derived` has unimplemented abstract methods + --> test.py:11:7 + | + 10 | @final + 11 | class Derived(Base): + | ^^^^^^^ `foo` is unimplemented + 12 | pass + | + ::: test.py:7:9 + | + 5 | class Base(ABC): + 6 | @abstractmethod + 7 | def foo(self) -> int: + | --- `foo` declared as abstract on superclass `Base` + 8 | raise NotImplementedError + | + info: rule `abstract-method-in-final-class` was selected in the configuration file + + Found 1 diagnostic + + ----- stderr ----- + "); + + Ok(()) +} + +/// Same TOML key ordering issue, but within an override's `[rules]` table. +/// `abstract-method-in-final-class` sorts before `all` lexicographically, but +/// the specific rule should still take precedence over `all`. +#[test] +fn overrides_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [[tool.ty.overrides]] + include = ["src/**"] + + [tool.ty.overrides.rules] + all = "warn" + abstract-method-in-final-class = "error" + "#, + ), + ( + "src/test.py", + r#" + from typing import final + from abc import ABC, abstractmethod + + class Base(ABC): + @abstractmethod + def foo(self) -> int: + raise NotImplementedError + + @final + class Derived(Base): + pass + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @" + success: false + exit_code: 1 + ----- stdout ----- + error[abstract-method-in-final-class]: Final class `Derived` has unimplemented abstract methods + --> src/test.py:11:7 + | + 10 | @final + 11 | class Derived(Base): + | ^^^^^^^ `foo` is unimplemented + 12 | pass + | + ::: src/test.py:7:9 + | + 5 | class Base(ABC): + 6 | @abstractmethod + 7 | def foo(self) -> int: + | --- `foo` declared as abstract on superclass `Base` + 8 | raise NotImplementedError + | + info: rule `abstract-method-in-final-class` was selected in the configuration file + + Found 1 diagnostic + + ----- stderr ----- + "); + + Ok(()) +} diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 862fdebb84287..7e900e5a978db 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -22,6 +22,7 @@ use ruff_python_ast::PythonVersion; use rustc_hash::FxHasher; use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use std::cmp::Ordering; use std::fmt::{self, Debug, Display}; use std::hash::BuildHasherDefault; use std::ops::Deref; @@ -107,10 +108,42 @@ pub struct Options { impl Options { pub fn from_toml_str(content: &str, source: ValueSource) -> Result { let _guard = ValueSourceGuard::new(source, true); - let options = toml::from_str(content)?; + let mut options: Self = toml::from_str(content)?; + options.prioritize_all_selectors(); Ok(options) } + /// Ensures that the `all` selector is applied before per-rule selectors + /// in all rule tables (top-level and overrides). + /// + /// This must be called after deserializing from TOML and before any + /// [`Combine::combine`] calls, because TOML tables are unordered and the + /// `toml` crate sorts keys lexicographically. + pub(crate) fn prioritize_all_selectors(&mut self) { + // Stable sort that moves all `all` selectors before non-`all` selectors + // while preserving relative order among non-`all` entries. + let sort = |rules: &mut Rules| { + rules.inner.sort_by( + |key_a, _, key_b, _| match (**key_a == "all", **key_b == "all") { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + _ => Ordering::Equal, + }, + ); + }; + + if let Some(rules) = &mut self.rules { + sort(rules); + } + if let Some(overrides) = &mut self.overrides { + for override_option in &mut overrides.0 { + if let Some(rules) = &mut override_option.rules { + sort(rules); + } + } + } + } + pub fn deserialize_with<'de, D>(source: ValueSource, deserializer: D) -> Result where D: serde::Deserializer<'de>, diff --git a/crates/ty_project/src/metadata/pyproject.rs b/crates/ty_project/src/metadata/pyproject.rs index 21dd5f02380d4..ca25d05a617fb 100644 --- a/crates/ty_project/src/metadata/pyproject.rs +++ b/crates/ty_project/src/metadata/pyproject.rs @@ -35,7 +35,16 @@ impl PyProject { source: ValueSource, ) -> Result { let _guard = ValueSourceGuard::new(source, true); - toml::from_str(content).map_err(PyProjectError::TomlSyntax) + let mut pyproject: Self = toml::from_str(content).map_err(PyProjectError::TomlSyntax)?; + // TOML tables are unordered and the `toml` crate sorts keys + // lexicographically. Normalize rule order so that the `all` selector + // is applied before per-rule selectors. + if let Some(tool) = &mut pyproject.tool { + if let Some(ty) = &mut tool.ty { + ty.prioritize_all_selectors(); + } + } + Ok(pyproject) } } From 3dc78b0a84ee231afb1c3329e11bfc912c236366 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Mar 2026 10:30:55 -0500 Subject: [PATCH 210/261] [ty] Use `HasOptionalDefinition` for `except` handlers (#23739) ## Summary See: https://github.com/astral-sh/ruff/pull/23708#discussion_r2888278545. --- crates/ty_ide/src/goto.rs | 10 ++-- crates/ty_python_semantic/src/lib.rs | 3 +- .../ty_python_semantic/src/semantic_model.rs | 56 +++++++++---------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index dae5ec67cdd60..4f3385fa63132 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -21,8 +21,8 @@ use ty_python_semantic::types::ide_support::{ typed_dict_key_definition, }; use ty_python_semantic::{ - HasDefinition, HasType, ImportAliasResolution, SemanticModel, TypeQualifiers, - definitions_for_imported_symbol, definitions_for_name, + HasDefinition, HasOptionalDefinition, HasType, ImportAliasResolution, SemanticModel, + TypeQualifiers, definitions_for_imported_symbol, definitions_for_name, }; #[derive(Clone, Debug)] @@ -331,7 +331,7 @@ impl GotoTarget<'_> { GotoTarget::ImportSymbolAlias { alias, .. } | GotoTarget::ImportModuleAlias { alias, .. } | GotoTarget::ImportExportedName { alias, .. } => alias.inferred_type(model), - GotoTarget::ExceptVariable(except) => model.except_handler_type(except), + GotoTarget::ExceptVariable(except) => except.inferred_type(model), GotoTarget::KeywordArgument { keyword, .. } => keyword.value.inferred_type(model), // When asking the type of a callable, usually you want the callable itself? // (i.e. the type of `MyClass` in `MyClass()` is `` and not `() -> MyClass`) @@ -515,8 +515,8 @@ impl GotoTarget<'_> { )), // For exception variables, they are their own definitions (like parameters) - GotoTarget::ExceptVariable(except_handler) => model - .except_handler_definition(except_handler) + GotoTarget::ExceptVariable(except_handler) => except_handler + .optional_definition(model) .map(|definition| vec![ResolvedDefinition::Definition(definition)]), // Patterns are glorified assignments but we have to look them up by ident diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index afb5f55f6480f..3d016805c6387 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -14,7 +14,8 @@ pub use program::{Program, ProgramSettings}; pub use python_platform::PythonPlatform; use rustc_hash::FxHasher; pub use semantic_model::{ - Completion, HasDefinition, HasType, MemberDefinition, NameKind, SemanticModel, + Completion, HasDefinition, HasOptionalDefinition, HasType, MemberDefinition, NameKind, + SemanticModel, }; pub use suppression::{ UNUSED_IGNORE_COMMENT, is_unused_ignore_comment_lint, suppress_all, suppress_single, diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 2aaadea41a811..7f22561f08ad0 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -304,14 +304,10 @@ impl<'db> SemanticModel<'db> { .scope(self.db) .file_scope_id(self.db), ), - ast::AnyNodeRef::ExceptHandlerExceptHandler(handler) => self - .except_handler_definition(handler) + ast::AnyNodeRef::ExceptHandlerExceptHandler(handler) => handler + .optional_definition(self) .map(|definition| definition.scope(self.db).file_scope_id(self.db)) - .or_else(|| { - handler.type_.as_deref().and_then(|handled_exceptions| { - index.try_expression_scope_id(handled_exceptions) - }) - }) + .or_else(|| index.try_expression_scope_id(handler.type_.as_deref()?)) .or(Some(FileScopeId::global())), ast::AnyNodeRef::TypeParamTypeVar(var) => { Some(var.definition(self).scope(self.db).file_scope_id(self.db)) @@ -328,29 +324,6 @@ impl<'db> SemanticModel<'db> { } } - /// Returns the definition for an exception-handler variable. - /// - /// Exception handlers only have a definition when they bind a name (`except E as name:`). - pub fn except_handler_definition( - &self, - handler: &ast::ExceptHandlerExceptHandler, - ) -> Option> { - handler.name.as_ref()?; - let index = semantic_index(self.db, self.file); - Some(index.expect_single_definition(handler)) - } - - /// Returns the inferred type of an exception-handler variable. - /// - /// Exception handlers only bind a variable when they have a name (`except E as name:`). - pub fn except_handler_type( - &self, - handler: &ast::ExceptHandlerExceptHandler, - ) -> Option> { - let definition = self.except_handler_definition(handler)?; - Some(binding_type(self.db, definition)) - } - /// Get a "safe" [`ast::AnyNodeRef`] to use for referring to the given (sub-)AST node. /// /// If we're analyzing a string annotation, it will return the string literal's node. @@ -543,6 +516,14 @@ pub trait HasDefinition { fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db>; } +pub trait HasOptionalDefinition { + /// Returns the definition of `self`, if it has one. + /// + /// ## Panics + /// May panic if `self` is from another file than `model`. + fn optional_definition<'db>(&self, model: &SemanticModel<'db>) -> Option>; +} + impl HasType for ast::ExprRef<'_> { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let index = semantic_index(model.db, model.file); @@ -679,6 +660,21 @@ impl HasType for ast::Alias { } } +impl HasOptionalDefinition for ast::ExceptHandlerExceptHandler { + fn optional_definition<'db>(&self, model: &SemanticModel<'db>) -> Option> { + self.name.as_ref()?; + let index = semantic_index(model.db, model.file); + Some(index.expect_single_definition(self)) + } +} + +impl HasType for ast::ExceptHandlerExceptHandler { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { + let definition = self.optional_definition(model)?; + Some(binding_type(model.db, definition)) + } +} + /// Implemented by types for which the semantic index tracks their scope. pub(crate) trait HasTrackedScope: HasNodeIndex {} From 9a70f5eb2fb0180953418cd6ac037cb3d531e77b Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Thu, 5 Mar 2026 09:55:23 -0800 Subject: [PATCH 211/261] Discover markdown files by default in preview mode (#23434) Adds `*.md` to the list of default globs in preview mode. Adds a simple filter to the `check` CLI command to exclude file types that aren't supported for linting before checking if the set of resolved paths is empty, preserving existing behavior of warning "No Python files found under the given path(s)" even if there are markdown files present and preview mode is enabled. Fixes #3792 --- crates/ruff/src/commands/add_noqa.rs | 26 ++++++--- crates/ruff/src/commands/analyze_graph.rs | 34 +++++------- crates/ruff/src/commands/check.rs | 17 +++++- crates/ruff/src/commands/check_stdin.rs | 4 +- crates/ruff/src/commands/format.rs | 4 +- crates/ruff/src/commands/format_stdin.rs | 4 +- crates/ruff/src/commands/show_files.rs | 17 +++++- crates/ruff/src/commands/show_settings.rs | 4 +- ...quires_python_no_tool_preview_enabled.snap | 1 + crates/ruff_dev/src/format_dev.rs | 4 +- crates/ruff_workspace/src/options.rs | 2 +- crates/ruff_workspace/src/resolver.rs | 12 ++-- crates/ruff_workspace/src/settings.rs | 1 + docs/formatter.md | 55 ++++++------------- 14 files changed, 98 insertions(+), 87 deletions(-) diff --git a/crates/ruff/src/commands/add_noqa.rs b/crates/ruff/src/commands/add_noqa.rs index 928c5ce03e5b3..f46deeecf1e55 100644 --- a/crates/ruff/src/commands/add_noqa.rs +++ b/crates/ruff/src/commands/add_noqa.rs @@ -11,7 +11,7 @@ use ruff_linter::source_kind::SourceKind; use ruff_linter::warn_user_once; use ruff_python_ast::{PySourceType, SourceType}; use ruff_workspace::resolver::{ - PyprojectConfig, ResolvedFile, match_exclusion, python_files_in_path, + PyprojectConfig, ResolvedFile, match_exclusion, project_files_in_path, }; use crate::args::ConfigArguments; @@ -25,10 +25,22 @@ pub(crate) fn add_noqa( ) -> Result { // Collect all the files to check. let start = Instant::now(); - let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; + let (mut paths, resolver) = project_files_in_path(files, pyproject_config, config_arguments)?; let duration = start.elapsed(); debug!("Identified files to lint in: {duration:?}"); + // Filter out paths for file types not supported for linting + paths.retain(|path| { + if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path { + matches!( + SourceType::from(path), + SourceType::Python(PySourceType::Python | PySourceType::Stub) + ) + } else { + true + } + }); + if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); return Ok(0); @@ -48,11 +60,7 @@ pub(crate) fn add_noqa( .par_iter() .flatten() .filter_map(|resolved_file| { - let SourceType::Python(source_type @ (PySourceType::Python | PySourceType::Stub)) = - SourceType::from(resolved_file.path()) - else { - return None; - }; + let source_type = SourceType::from(resolved_file.path()); let path = resolved_file.path(); let package = resolved_file .path() @@ -69,7 +77,7 @@ pub(crate) fn add_noqa( { return None; } - let source_kind = match SourceKind::from_path(path, SourceType::Python(source_type)) { + let source_kind = match SourceKind::from_path(path, source_type) { Ok(Some(source_kind)) => source_kind, Ok(None) => return None, Err(e) => { @@ -81,7 +89,7 @@ pub(crate) fn add_noqa( path, package, &source_kind, - source_type, + source_type.expect_python(), &settings.linter, reason, ) { diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index e36173c129715..968dac4c355a2 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -11,7 +11,7 @@ use ruff_linter::package::PackageRoot; use ruff_linter::source_kind::SourceKind; use ruff_linter::{warn_user, warn_user_once}; use ruff_python_ast::SourceType; -use ruff_workspace::resolver::{ResolvedFile, match_exclusion, python_files_in_path}; +use ruff_workspace::resolver::{ResolvedFile, match_exclusion, project_files_in_path}; use rustc_hash::{FxBuildHasher, FxHashMap}; use std::io::Write; use std::path::{Path, PathBuf}; @@ -35,7 +35,16 @@ pub(crate) fn analyze_graph( // Find all Python files. let files = resolve_default_files(args.files, false); - let (paths, resolver) = python_files_in_path(&files, &pyproject_config, config_arguments)?; + let (mut paths, resolver) = project_files_in_path(&files, &pyproject_config, config_arguments)?; + + // Filter to only Python files + paths.retain(|path| { + if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path { + matches!(SourceType::from(path), SourceType::Python(_)) + } else { + true + } + }); if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); @@ -124,6 +133,7 @@ pub(crate) fn analyze_graph( let string_imports = settings.analyze.string_imports; let include_dependencies = settings.analyze.include_dependencies.get(path).cloned(); let type_checking_imports = settings.analyze.type_checking_imports; + let source_type = settings.analyze.extension.get_source_type(path); // Skip excluded files. if (settings.file_resolver.force_exclude || !resolved_file.is_root()) @@ -136,19 +146,6 @@ pub(crate) fn analyze_graph( continue; } - // Ignore non-Python files. - let source_type = match settings.analyze.extension.get_source_type(path) { - SourceType::Python(source_type) => source_type, - SourceType::Toml(_) => { - debug!("Ignoring TOML file: {}", path.display()); - continue; - } - SourceType::Markdown => { - debug!("Ignoring Markdown file: {}", path.display()); - continue; - } - }; - // Convert to system paths. let Ok(package) = package.map(SystemPathBuf::from_path_buf).transpose() else { warn!("Failed to convert package to system path"); @@ -165,10 +162,7 @@ pub(crate) fn analyze_graph( let result = inner_result.clone(); scope.spawn(move |_| { // Extract source code (handles both .py and .ipynb files) - let source_kind = match SourceKind::from_path( - path.as_std_path(), - SourceType::Python(source_type), - ) { + let source_kind = match SourceKind::from_path(path.as_std_path(), source_type) { Ok(Some(source_kind)) => source_kind, Ok(None) => { debug!("Skipping non-Python notebook: {path}"); @@ -186,7 +180,7 @@ pub(crate) fn analyze_graph( let mut imports = ModuleImports::detect( &db, source_code, - source_type, + source_type.expect_python(), &path, package.as_deref(), string_imports, diff --git a/crates/ruff/src/commands/check.rs b/crates/ruff/src/commands/check.rs index 694b1eb8f2ad7..5a80e4f5ac3b2 100644 --- a/crates/ruff/src/commands/check.rs +++ b/crates/ruff/src/commands/check.rs @@ -10,6 +10,7 @@ use log::{debug, warn}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; use ruff_linter::message::create_panic_diagnostic; +use ruff_python_ast::{SourceType, TomlSourceType}; use rustc_hash::FxHashMap; use ruff_db::diagnostic::Diagnostic; @@ -22,7 +23,7 @@ use ruff_linter::{IOError, Violation, fs, warn_user_once}; use ruff_source_file::SourceFileBuilder; use ruff_text_size::TextRange; use ruff_workspace::resolver::{ - PyprojectConfig, ResolvedFile, match_exclusion, python_files_in_path, + PyprojectConfig, ResolvedFile, match_exclusion, project_files_in_path, }; use crate::args::ConfigArguments; @@ -41,9 +42,21 @@ pub(crate) fn check( ) -> Result { // Collect all the Python files to check. let start = Instant::now(); - let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; + let (mut paths, resolver) = project_files_in_path(files, pyproject_config, config_arguments)?; debug!("Identified files to lint in: {:?}", start.elapsed()); + // Filter out paths for file types not supported for linting + paths.retain(|path| { + if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path { + matches!( + SourceType::from(path), + SourceType::Python(_) | SourceType::Toml(TomlSourceType::Pyproject) + ) + } else { + true + } + }); + if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); return Ok(Diagnostics::default()); diff --git a/crates/ruff/src/commands/check_stdin.rs b/crates/ruff/src/commands/check_stdin.rs index f76b3ab2df69e..0df58a806031b 100644 --- a/crates/ruff/src/commands/check_stdin.rs +++ b/crates/ruff/src/commands/check_stdin.rs @@ -5,7 +5,7 @@ use ruff_db::diagnostic::Diagnostic; use ruff_linter::package::PackageRoot; use ruff_linter::packaging; use ruff_linter::settings::flags; -use ruff_workspace::resolver::{PyprojectConfig, Resolver, match_exclusion, python_file_at_path}; +use ruff_workspace::resolver::{PyprojectConfig, Resolver, match_exclusion, project_file_at_path}; use crate::args::ConfigArguments; use crate::diagnostics::{Diagnostics, lint_stdin}; @@ -23,7 +23,7 @@ pub(crate) fn check_stdin( if resolver.force_exclude() { if let Some(filename) = filename { - if !python_file_at_path(filename, &mut resolver, overrides)? { + if !project_file_at_path(filename, &mut resolver, overrides)? { if fix_mode.is_apply() { parrot_stdin()?; } diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 868493caf6bf6..fdf593d0c5393 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -38,7 +38,7 @@ use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceFileBuilder}; use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_workspace::FormatterSettings; use ruff_workspace::resolver::{ - PyprojectConfig, ResolvedFile, Resolver, match_exclusion, python_files_in_path, + PyprojectConfig, ResolvedFile, Resolver, match_exclusion, project_files_in_path, }; use crate::args::{ConfigArguments, FormatArguments, FormatRange}; @@ -75,7 +75,7 @@ pub(crate) fn format( ) -> Result { let mode = FormatMode::from_cli(&cli); let files = resolve_default_files(cli.files, false); - let (paths, resolver) = python_files_in_path(&files, pyproject_config, config_arguments)?; + let (paths, resolver) = project_files_in_path(&files, pyproject_config, config_arguments)?; let output_format = pyproject_config.settings.output_format; let preview = pyproject_config.settings.formatter.preview; diff --git a/crates/ruff/src/commands/format_stdin.rs b/crates/ruff/src/commands/format_stdin.rs index aba5e31cbc26b..e75dd64b1fb79 100644 --- a/crates/ruff/src/commands/format_stdin.rs +++ b/crates/ruff/src/commands/format_stdin.rs @@ -7,7 +7,7 @@ use log::error; use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_python_ast::SourceType; use ruff_workspace::FormatterSettings; -use ruff_workspace::resolver::{PyprojectConfig, Resolver, match_exclusion, python_file_at_path}; +use ruff_workspace::resolver::{PyprojectConfig, Resolver, match_exclusion, project_file_at_path}; use crate::ExitStatus; use crate::args::{ConfigArguments, FormatArguments, FormatRange}; @@ -30,7 +30,7 @@ pub(crate) fn format_stdin( if resolver.force_exclude() { if let Some(filename) = cli.stdin_filename.as_deref() { - if !python_file_at_path(filename, &mut resolver, config_arguments)? { + if !project_file_at_path(filename, &mut resolver, config_arguments)? { if mode.is_write() { parrot_stdin()?; } diff --git a/crates/ruff/src/commands/show_files.rs b/crates/ruff/src/commands/show_files.rs index 7c74837fd3495..22826dbd0d5b9 100644 --- a/crates/ruff/src/commands/show_files.rs +++ b/crates/ruff/src/commands/show_files.rs @@ -5,7 +5,8 @@ use anyhow::Result; use itertools::Itertools; use ruff_linter::warn_user_once; -use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, python_files_in_path}; +use ruff_python_ast::{SourceType, TomlSourceType}; +use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, project_files_in_path}; use crate::args::ConfigArguments; @@ -17,7 +18,19 @@ pub(crate) fn show_files( writer: &mut impl Write, ) -> Result<()> { // Collect all files in the hierarchy. - let (paths, _resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; + let (mut paths, _resolver) = project_files_in_path(files, pyproject_config, config_arguments)?; + + // Filter out paths for file types not supported for linting + paths.retain(|path| { + if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path { + matches!( + SourceType::from(path), + SourceType::Python(_) | SourceType::Toml(TomlSourceType::Pyproject) + ) + } else { + true + } + }); if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); diff --git a/crates/ruff/src/commands/show_settings.rs b/crates/ruff/src/commands/show_settings.rs index 8c38285955e7f..ccea749ae20a7 100644 --- a/crates/ruff/src/commands/show_settings.rs +++ b/crates/ruff/src/commands/show_settings.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use anyhow::{Result, bail}; use itertools::Itertools; -use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, python_files_in_path}; +use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, project_files_in_path}; use crate::args::ConfigArguments; @@ -16,7 +16,7 @@ pub(crate) fn show_settings( writer: &mut impl Write, ) -> Result<()> { // Collect all files in the hierarchy. - let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; + let (paths, resolver) = project_files_in_path(files, pyproject_config, config_arguments)?; // Print the list of files. let Some(path) = paths diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 885ea0a77fa49..3ada1e2365f76 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -63,6 +63,7 @@ file_resolver.include = [ "*.pyw", "*.ipynb", "**/pyproject.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index 82932e76fc7e3..0f25e87bc3553 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -36,7 +36,7 @@ use ruff_python_formatter::{ FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions, format_module_source, }; use ruff_python_parser::ParseError; -use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, Resolver, python_files_in_path}; +use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, Resolver, project_files_in_path}; fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, ConfigArguments)> { let args_matches = FormatCommand::command() @@ -68,7 +68,7 @@ fn ruff_check_paths<'a>( cli: &FormatArguments, config_arguments: &ConfigArguments, ) -> anyhow::Result<(Vec>, Resolver<'a>)> { - let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, config_arguments)?; + let (paths, resolver) = project_files_in_path(&cli.files, pyproject_config, config_arguments)?; Ok((paths, resolver)) } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 0f8697445ec9d..bc6339c5c4a7b 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -262,7 +262,7 @@ pub struct Options { /// /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). #[option( - default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "**/pyproject.toml"]"#, + default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "*.md", "**/pyproject.toml"]"#, value_type = "list[str]", example = r#" include = ["*.py"] diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index c253d3fc83e2b..0dfb203aad2ff 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -432,8 +432,8 @@ impl From for Relativity { } } -/// Find all Python (`.py`, `.pyi`, `.pyw`, and `.ipynb` files) in a set of paths. -pub fn python_files_in_path<'a>( +/// Find all project files in a set of paths, following configured include/exclude settings. +pub fn project_files_in_path<'a>( paths: &[PathBuf], pyproject_config: &'a PyprojectConfig, transformer: &(dyn ConfigurationTransformer + Sync), @@ -504,7 +504,7 @@ pub fn python_files_in_path<'a>( let walker = builder.build_parallel(); - // Run the `WalkParallel` to collect all Python files. + // Run the `WalkParallel` to collect all files. let state = WalkPythonFilesState::new(resolver); let mut visitor = PythonFilesVisitorBuilder::new(transformer, &state); walker.visit(&mut visitor); @@ -762,7 +762,7 @@ impl ResolvedFile { } /// Return `true` if the Python file at [`Path`] is _not_ excluded. -pub fn python_file_at_path( +pub fn project_file_at_path( path: &Path, resolver: &mut Resolver, transformer: &dyn ConfigurationTransformer, @@ -962,7 +962,7 @@ mod tests { use crate::pyproject::find_settings_toml; use crate::resolver::{ ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, - ResolvedFile, Resolver, is_file_excluded, match_exclusion, python_files_in_path, + ResolvedFile, Resolver, is_file_excluded, match_exclusion, project_files_in_path, resolve_root_settings, }; use crate::settings::Settings; @@ -1024,7 +1024,7 @@ mod tests { File::create(&file2)?; create_dir(dir2)?; - let (paths, _) = python_files_in_path( + let (paths, _) = project_files_in_path( &[root.to_path_buf()], &PyprojectConfig::new(PyprojectDiscoveryStrategy::Fixed, Settings::default(), None), &NoOpTransformer, diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 6e3159e5266dc..98befe4b775fd 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -150,6 +150,7 @@ pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.pyw"), FilePattern::Builtin("*.ipynb"), FilePattern::Builtin("**/pyproject.toml"), + FilePattern::Builtin("*.md"), ]; impl FileResolverSettings { diff --git a/docs/formatter.md b/docs/formatter.md index ebf09308aa5ad..4bd968d7f1c3b 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -285,43 +285,6 @@ to `` and `` respectively. [blacken-docs]: https://github.com/adamchainz/blacken-docs/ -Ruff will not automatically discover or format Markdown files in your project, -but will format any Markdown files explicitly passed with a `.md` extension: - -```shell-session -$ ruff format --preview --check docs/ -warning: No Python files found under the given path(s) - -$ ruff format --preview --check docs/*.md -13 files already formatted -``` - -This is likely to change in a future release when the feature is stabilized. -Including Markdown files without also enabling [preview mode](preview.md#preview) -will result in an error message and non-zero [exit code](#exit-codes). - -To include Markdown files by default when running Ruff on your project, add them -with [`extend-include`](settings.md#extend-include) in your project settings: - -=== "pyproject.toml" - - ```toml - [tool.ruff] - # Find and format code blocks in Markdown files - extend-include = ["*.md"] - # OR - extend-include = ["docs/*.md"] - ``` - -=== "ruff.toml" - - ```toml - # Find and format code blocks in Markdown files - extend-include = ["*.md"] - # OR - extend-include = ["docs/*.md"] - ``` - To format Markdown files with extensions other than `.md`, configure custom [`extension`](settings.md#extension) mappings. Ruff will automatically include these mapped extensions in file discovery: @@ -353,6 +316,24 @@ repos: types_or: [python, pyi, jupyter, markdown] ``` +To *disable* formatting of Markdown files, add them to +[`extend-exclude`](settings.md#extend-exclude) in your project settings: + +=== "pyproject.toml" + + ```toml + [tool.ruff] + # Disable formatting in Markdown files + extend-exclude = ["*.md"] + ``` + +=== "ruff.toml" + + ```toml + # Disable formatting in Markdown files + extend-exclude = ["*.md"] + ``` + ## Format suppression Like Black, Ruff supports `# fmt: on`, `# fmt: off`, and `# fmt: skip` pragma comments, which can From 4926bd58204839cb75a8ed1397e824bbc8f644ca Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 5 Mar 2026 18:11:25 +0000 Subject: [PATCH 212/261] [ty] Split deferred checks out of `types/infer/builder.rs` (#23740) --- crates/ty_python_semantic/src/types/infer.rs | 1 + .../src/types/infer/builder.rs | 2501 ++--------------- .../src/types/infer/deferred/dynamic_class.rs | 95 + .../types/infer/deferred/final_variable.rs | 67 + .../src/types/infer/deferred/function.rs | 341 +++ .../src/types/infer/deferred/mod.rs | 9 + .../infer/deferred/overloaded_function.rs | 249 ++ .../src/types/infer/deferred/static_class.rs | 1260 +++++++++ .../src/types/infer/deferred/typeguard.rs | 76 + 9 files changed, 2300 insertions(+), 2299 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/infer/deferred/dynamic_class.rs create mode 100644 crates/ty_python_semantic/src/types/infer/deferred/final_variable.rs create mode 100644 crates/ty_python_semantic/src/types/infer/deferred/function.rs create mode 100644 crates/ty_python_semantic/src/types/infer/deferred/mod.rs create mode 100644 crates/ty_python_semantic/src/types/infer/deferred/overloaded_function.rs create mode 100644 crates/ty_python_semantic/src/types/infer/deferred/static_class.rs create mode 100644 crates/ty_python_semantic/src/types/infer/deferred/typeguard.rs diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 3ea857779e333..185b83d98ebcf 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -61,6 +61,7 @@ pub(super) use comparisons::UnsupportedComparisonError; mod builder; mod comparisons; +mod deferred; #[cfg(test)] mod tests; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 862f68851e913..29f34170b1a91 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1,24 +1,21 @@ use std::borrow::Cow; use itertools::{Either, Itertools}; -use ruff_db::diagnostic::{ - Annotation, DiagnosticId, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, -}; +use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::File; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::source::source_text; -use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::name::Name; use ruff_python_ast::visitor::{Visitor, walk_expr}; use ruff_python_ast::{ - self as ast, AnyNodeRef, AnyParameterRef, ArgOrKeyword, ArgumentsSourceOrder, ExprContext, - HasNodeIndex, NodeIndex, PythonVersion, + self as ast, AnyNodeRef, ArgOrKeyword, ArgumentsSourceOrder, ExprContext, HasNodeIndex, + NodeIndex, PythonVersion, }; use ruff_python_stdlib::builtins::version_builtin_was_added; use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_stdlib::keyword::is_keyword; use ruff_python_stdlib::typing::as_pep_585_generic; -use ruff_text_size::{Ranged, TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use ty_module_resolver::{ @@ -26,6 +23,7 @@ use ty_module_resolver::{ resolve_module, search_paths, }; +use super::deferred; use super::{ DefinitionInference, DefinitionInferenceExtra, ExpressionInference, ExpressionInferenceExtra, InferenceRegion, ScopeInference, ScopeInferenceExtra, infer_deferred_types, @@ -56,58 +54,50 @@ use crate::semantic_index::scope::{ }; use crate::semantic_index::symbol::{ScopedSymbolId, Symbol}; use crate::semantic_index::{ - ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, attribute_assignments, - place_table, + ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, }; use crate::types::BindingContext; +use crate::types::CallableTypes; use crate::types::call::bind::MatchingOverloadIndex; -use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind}; +use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::callable::CallableTypeKind; use crate::types::class::{ - AbstractMethod, ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral, - DynamicMetaclassConflict, DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, FieldKind, - MetaclassErrorKind, MethodDecorator, NamedTupleField, NamedTupleSpec, + ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral, + DynamicMetaclassConflict, DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, MethodDecorator, + NamedTupleField, NamedTupleSpec, }; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::diagnostic::{ - self, ABSTRACT_METHOD_IN_FINAL_CLASS, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, - CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, - DATACLASS_FIELD_ORDER, DUPLICATE_BASE, DUPLICATE_KW_ONLY, FINAL_ON_NON_METHOD, - FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, - INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DATACLASS, - INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_GENERIC_CLASS, - INVALID_GENERIC_ENUM, INVALID_KEY, INVALID_LEGACY_POSITIONAL_PARAMETER, - INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, - INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, - INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, - INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, - INVALID_TYPE_VARIABLE_DEFAULT, INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, - IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, - POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, - SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, - UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, - USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, + self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CYCLIC_CLASS_DEFINITION, + CYCLIC_TYPE_ALIAS_DEFINITION, DUPLICATE_BASE, FINAL_ON_NON_METHOD, INCONSISTENT_MRO, + INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, + INVALID_BASE, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_KEY, + INVALID_LEGACY_TYPE_VARIABLE, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_PARAMETER_DEFAULT, + INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, + INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, + INVALID_TYPE_VARIABLE_DEFAULT, IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, + PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, + POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, + TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, + UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, + UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, USELESS_OVERLOAD_BODY, + hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, - report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, - report_call_to_abstract_method, report_cannot_delete_typed_dict_key, - report_cannot_pop_required_field_on_typed_dict, report_conflicting_metaclass_from_bases, - report_duplicate_bases, report_implicit_return_type, report_instance_layout_conflict, - report_invalid_assignment, report_invalid_attribute_assignment, - report_invalid_class_match_pattern, report_invalid_exception_caught, - report_invalid_exception_cause, report_invalid_exception_raised, - report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, - report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, - report_invalid_return_type, report_invalid_total_ordering, - report_invalid_type_checking_constant, report_invalid_type_param_order, - report_invalid_typevar_default_reference, + report_bad_dunder_set_call, report_call_to_abstract_method, + report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict, + report_conflicting_metaclass_from_bases, report_implicit_return_type, + report_instance_layout_conflict, report_invalid_assignment, + report_invalid_attribute_assignment, report_invalid_class_match_pattern, + report_invalid_exception_caught, report_invalid_exception_cause, + report_invalid_exception_raised, report_invalid_exception_tuple_caught, + report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, + report_invalid_return_type, report_invalid_type_checking_constant, report_match_pattern_against_non_runtime_checkable_protocol, - report_match_pattern_against_typed_dict, report_named_tuple_field_with_leading_underscore, - report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable, + report_match_pattern_against_typed_dict, report_not_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_shadowed_type_variable, report_unsupported_augmented_assignment, - report_unsupported_base, report_unsupported_comparison, + report_unsupported_comparison, }; use crate::types::enums::{enum_ignored_names, is_enum_class_by_inheritance}; use crate::types::function::{ @@ -119,7 +109,7 @@ use crate::types::generics::{ }; use crate::types::infer::builder::paramspec_validation::validate_paramspec_components; use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function}; -use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; +use crate::types::mro::DynamicMroErrorKind; use crate::types::newtype::NewType; use crate::types::set_theoretic::RecursivelyDefined; use crate::types::subclass_of::SubclassOfInner; @@ -136,16 +126,14 @@ use crate::types::typevar::{ use crate::types::visitor::find_over_type; use crate::types::{ CallDunderError, CallableBinding, CallableType, ClassType, DataclassParams, DynamicType, - EvaluationMode, GenericAlias, InferenceFlags, InternedConstraintSet, InternedType, - IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, - LintDiagnosticGuard, LiteralValueTypeKind, MemberLookupPolicy, MetaclassCandidate, - ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, - StaticClassLiteral, SubclassOfType, Truthiness, Type, TypeAliasType, TypeAndQualifiers, - TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, - TypedDictType, UnionBuilder, UnionType, binding_type, definition_expression_type, - infer_complete_scope_types, infer_scope_types, todo_type, + EvaluationMode, InferenceFlags, InternedConstraintSet, InternedType, IntersectionBuilder, + IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, + LiteralValueTypeKind, MemberLookupPolicy, ParamSpecAttrKind, Parameter, ParameterForm, + Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, + TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, binding_type, + definition_expression_type, infer_complete_scope_types, infer_scope_types, todo_type, }; -use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::UnpackPosition; use crate::{AnalysisSettings, Db, FxIndexSet, Program}; @@ -275,9 +263,9 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { /// A set of functions that have been defined **and** called in this region. /// /// This is a set because the same function could be called multiple times in the same region. - /// This is mainly used in [`check_overloaded_functions`] to check an overloaded function that - /// is shadowed by a function with the same name in this scope but has been called before. For - /// example: + /// This is mainly used in [`deferred::overloaded_function::check_overloaded_function`] to + /// check an overloaded function that is shadowed by a function with the same name in this + /// scope but has been called before. For example: /// /// ```py /// from typing import overload @@ -295,8 +283,6 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { /// ``` /// /// To keep the calculation deterministic, we use an `FxIndexSet` whose order is determined by the sequence of insertion calls. - /// - /// [`check_overloaded_functions`]: TypeInferenceBuilder::check_overloaded_functions called_functions: FxIndexSet>, /// Whether we are in a context that binds unbound typevars. @@ -669,2062 +655,81 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx, ); } - NodeWithScopeKind::GeneratorExpression(generator) => { - self.infer_generator_expression_scope(generator.node(self.module())); - } - } - - // Infer deferred types for all definitions. - let deferred_definitions: Vec<_> = std::mem::take(&mut self.deferred).into_iter().collect(); - for definition in &deferred_definitions { - self.extend_definition(infer_deferred_types(self.db(), *definition)); - } - - assert!( - self.deferred.is_empty(), - "Inferring deferred types should not add more deferred definitions" - ); - - if self.db().should_check_file(self.file()) { - self.check_static_class_definitions(); - self.check_dynamic_class_definitions(&deferred_definitions); - self.check_overloaded_functions(node); - self.check_type_guard_definitions(); - self.check_function_definitions(); - self.check_final_without_value(); - } - } - - /// Iterate over all function definitions in this scope and run checks that - /// require access to the function's inferred signature (and therefore must - /// run after deferred inference is complete). - fn check_function_definitions(&self) { - let db = self.db(); - - for (definition, _) in &self.declarations { - if !definition.kind(db).is_function_def() { - continue; - } - let Some(Type::FunctionLiteral(function_type)) = - infer_definition_types(db, *definition).undecorated_type() - else { - continue; - }; - - let last_definition = function_type.literal(db).last_definition(db); - let signature = last_definition.raw_signature(db); - - self.check_legacy_positional_only_convention(last_definition, &signature); - self.check_legacy_typevar_defaults(last_definition, &signature); - self.check_legacy_typevar_ordering(last_definition, &signature); - } - } - - /// Check for invalid applications of the pre-PEP-570 positional-only parameter convention. - fn check_legacy_positional_only_convention( - &self, - last_definition: OverloadLiteral<'db>, - signature: &Signature<'db>, - ) { - let node = last_definition.node(self.db(), self.file(), self.module()); - let ast_parameters = &node.parameters; - - // If the function has any PEP-570 positional-only parameters, - // assume that `__`-prefixed parameters are not meant to be positional-only - if !ast_parameters.posonlyargs.is_empty() { - return; - } - let parsed_parameters = signature.parameters(); - let mut previous_non_positional_only: Option<&ast::ParameterWithDefault> = None; - - for (param_node, param) in std::iter::zip(ast_parameters, parsed_parameters) { - let AnyParameterRef::NonVariadic(param_node) = param_node else { - continue; - }; - if param.is_positional_only() { - continue; - } - - // Valid uses of the PEP-484 positional-only convention will have been detected as such - // in the first iteration over this scope, so `param.is_positional_only()` will return `true` - // for those. We only get here for invalid uses of the PEP-484 positional-only convention. - if param_node.uses_pep_484_positional_only_convention() { - let Some(builder) = self - .context - .report_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER, param_node.name()) - else { - continue; - }; - let mut diagnostic = builder.into_diagnostic( - "Invalid use of the legacy convention \ - for positional-only parameters", - ); - diagnostic.set_primary_message( - "Parameter name begins with `__` but will not be treated as positional-only", - ); - diagnostic.info( - "A parameter can only be positional-only \ - if it precedes all positional-or-keyword parameters", - ); - if let Some(earlier_node) = previous_non_positional_only { - diagnostic.annotate( - self.context - .secondary(earlier_node.name()) - .message("Prior parameter here was positional-or-keyword"), - ); - } - } else if previous_non_positional_only.is_none() { - previous_non_positional_only = Some(param_node); - } - } - } - - /// Find the range of the first parameter annotation (or return type) in a function - /// whose inferred type references the given `TypeVar`, falling back to the function name. - fn find_typevar_annotation_range( - &self, - node: &ast::StmtFunctionDef, - typevar: TypeVarInstance<'db>, - ) -> TextRange { - let db = self.db(); - let typevar_id = typevar.identity(self.db()); - - node.parameters - .iter() - .filter_map(ast::AnyParameterRef::annotation) - .chain(node.returns.as_deref()) - .find(|ann| { - self.file_expression_type(ann) - .references_typevar(db, typevar_id) - }) - .map(Ranged::range) - .unwrap_or(node.name.range()) - } - - /// Check whether any legacy `TypeVar` used in a function signature has a default - /// that references an out-of-scope type variable. - /// - /// This check mirrors the class-level check at `report_invalid_typevar_default_reference`, - /// but for function/method generic contexts. - fn check_legacy_typevar_defaults( - &self, - last_definition: OverloadLiteral<'db>, - signature: &Signature<'db>, - ) { - let db = self.db(); - - let Some(generic_context) = signature.generic_context else { - return; - }; - - let typevars = generic_context - .variables(db) - .map(|bound_tvar| bound_tvar.typevar(db)); - - for (i, typevar) in typevars.clone().enumerate() { - // Only check legacy TypeVars; PEP 695 type parameters are already validated - // by `check_default_for_outer_scope_typevars` in the type parameter scope. - if !matches!( - typevar.kind(db), - TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec - ) { - continue; - } - - let Some(default_ty) = typevar.default_type(db) else { - continue; - }; - - let first_bad_tvar = find_over_type(db, default_ty, false, |t| { - let tvar = match t { - Type::TypeVar(tvar) => tvar.typevar(db), - Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, - _ => return None, - }; - if !typevars.clone().take(i).contains(&tvar) { - Some(tvar) - } else { - None - } - }); - - let Some(bad_typevar) = first_bad_tvar else { - continue; - }; - - let is_later_in_list = typevars.clone().skip(i).contains(&bad_typevar); - let node = last_definition.node(db, self.file(), self.module()); - - let primary_range = self.find_typevar_annotation_range(node, typevar); - - let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, primary_range) - else { - continue; - }; - let typevar_name = typevar.name(db); - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid use of type variable `{typevar_name}`", - )); - - if is_later_in_list { - diagnostic.set_primary_message(format_args!( - "Default of `{typevar_name}` references later type parameter `{}`", - bad_typevar.name(db), - )); - diagnostic.set_concise_message(format_args!( - "Invalid use of type variable `{typevar_name}`: default of `{typevar_name}` \ - refers to later parameter `{}`", - bad_typevar.name(db) - )); - } else { - diagnostic.set_primary_message(format_args!( - "Default of `{typevar_name}` references out-of-scope type variable `{}`", - bad_typevar.name(db), - )); - diagnostic.set_concise_message(format_args!( - "Invalid use of type variable `{typevar_name}`: default of `{typevar_name}` \ - refers to out-of-scope type variable `{}`", - bad_typevar.name(db) - )); - } - - if let Some(typevar_definition) = typevar.definition(db) { - let file = typevar_definition.file(db); - diagnostic.annotate( - Annotation::secondary(Span::from( - typevar_definition.full_range(db, &parsed_module(db, file).load(db)), - )) - .message(format_args!("`{typevar_name}` defined here")), - ); - } - - diagnostic - .info("See https://typing.python.org/en/latest/spec/generics.html#scoping-rules"); - } - } - - /// Check that legacy `TypeVar`s without defaults don't follow `TypeVar`s with defaults - /// in a function's generic context. - /// - /// This mirrors the class-level check using `report_invalid_type_param_order`, but for - /// function/method generic contexts using the `invalid-type-variable-default` lint. - fn check_legacy_typevar_ordering( - &self, - last_definition: OverloadLiteral<'db>, - signature: &Signature<'db>, - ) { - struct State<'db> { - typevar_with_default: TypeVarInstance<'db>, - invalid_later_tvars: Vec>, - } - - let db = self.db(); - - let Some(generic_context) = signature.generic_context else { - return; - }; - - let mut state: Option> = None; - - for bound_typevar in generic_context.variables(db) { - let typevar = bound_typevar.typevar(db); - - // Only check legacy TypeVars; PEP 695 ordering is validated by the parser. - if !matches!( - typevar.kind(db), - TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec - ) { - continue; - } - - let has_default = typevar.default_type(db).is_some(); - - if let Some(state) = state.as_mut() { - if !has_default { - state.invalid_later_tvars.push(typevar); - } - } else if has_default { - state = Some(State { - typevar_with_default: typevar, - invalid_later_tvars: vec![], - }); - } - } - - let Some(state) = state else { - return; - }; - - if state.invalid_later_tvars.is_empty() { - return; - } - - let node = last_definition.node(db, self.file(), self.module()); - - let primary_range = self.find_typevar_annotation_range(node, state.invalid_later_tvars[0]); - - let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, primary_range) - else { - return; - }; - - let mut diagnostic = builder.into_diagnostic( - "Type parameters without defaults cannot follow type parameters with defaults", - ); - - let typevar_with_default_name = state.typevar_with_default.name(db); - - diagnostic.set_concise_message(format_args!( - "Type parameter `{}` without a default cannot follow \ - earlier parameter `{typevar_with_default_name}` with a default", - state.invalid_later_tvars[0].name(db), - )); - - if let [single_typevar] = &*state.invalid_later_tvars { - diagnostic.set_primary_message(format_args!( - "Type variable `{}` does not have a default", - single_typevar.name(db), - )); - } else { - let later_typevars = - format_enumeration(state.invalid_later_tvars.iter().map(|tv| tv.name(db))); - diagnostic.set_primary_message(format_args!( - "Type variables {later_typevars} do not have defaults", - )); - } - - let secondary_range = self.find_typevar_annotation_range(node, state.typevar_with_default); - - diagnostic.annotate( - self.context - .secondary(secondary_range) - .message(format_args!( - "Earlier TypeVar `{typevar_with_default_name}` has a default" - )), - ); - - for tvar in [state.typevar_with_default, state.invalid_later_tvars[0]] { - let Some(definition) = tvar.definition(db) else { - continue; - }; - let file = definition.file(db); - diagnostic.annotate( - Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), - )) - .message(format_args!("`{}` defined here", tvar.name(db))), - ); - } - } - - /// Iterate over all static class definitions (created using `class` statements) to check that - /// the definition will not cause an exception to be raised at runtime. This needs to be done - /// after most other types in the scope have been inferred, due to the fact that base classes - /// can be deferred. If it looks like a class definition is invalid in some way, issue a - /// diagnostic. - /// - /// Note: Dynamic classes created via `type()` calls are checked separately during type - /// inference of the call expression. - /// - /// Among the things we check for in this method are whether Python will be able to determine a - /// consistent "[method resolution order]" and [metaclass] for each class. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - /// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses - fn check_static_class_definitions(&mut self) { - let class_definitions = self.declarations.iter().filter_map(|(definition, ty)| { - // Filter out class literals that result from imports - if let DefinitionKind::Class(class) = definition.kind(self.db()) { - ty.inner_type() - .as_class_literal() - .and_then(ClassLiteral::as_static) - .map(|class_literal| (class_literal, class.node(self.module()))) - } else { - None - } - }); - - // Iterate through all class definitions in this scope. - for (class, class_node) in class_definitions { - // (1) Check that the class does not have a cyclic definition - if let Some(inheritance_cycle) = class.inheritance_cycle(self.db()) { - if inheritance_cycle.is_participant() - && let Some(builder) = self - .context - .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) - { - builder.into_diagnostic(format_args!( - "Cyclic definition of `{}` (class cannot inherit from itself)", - class.name(self.db()) - )); - } - - // If a class is cyclically defined, that's a sufficient error to report; the - // following checks (which are all inheritance-based) aren't even relevant. - continue; - } - - // (2) Check that the class is not an enum and generic - if is_enum_class_by_inheritance(self.db(), class) - && class.generic_context(self.db()).is_some() - { - if let Some(builder) = self.context.report_lint(&INVALID_GENERIC_ENUM, class_node) { - builder.into_diagnostic(format_args!( - "Enum class `{}` cannot be generic", - class.name(self.db()) - )); - } - } - - let class_kind = CodeGeneratorKind::from_class(self.db(), class.into(), None); - - // (3) If it's a `NamedTuple` class, check that no field without a default value - // appears after a field with a default value. - if class_kind == Some(CodeGeneratorKind::NamedTuple) { - let mut field_with_default_encountered = None; - - for (field_name, field) in - class.own_fields(self.db(), None, CodeGeneratorKind::NamedTuple) - { - if field_name.starts_with('_') { - report_named_tuple_field_with_leading_underscore( - &self.context, - class, - &field_name, - field.first_declaration, - ); - } - - if matches!( - field.kind, - FieldKind::NamedTuple { - default_ty: Some(_) - } - ) { - field_with_default_encountered = - Some((field_name, field.first_declaration)); - } else if let Some(field_with_default) = field_with_default_encountered.as_ref() - { - report_namedtuple_field_without_default_after_field_with_default( - &self.context, - class, - (&field_name, field.first_declaration), - field_with_default, - ); - } - } - } - - let is_protocol = class.is_protocol(self.db()); - - // (4) Check for invalid `@dataclass` applications. - if class.dataclass_params(self.db()).is_some() { - if class.has_named_tuple_class_in_mro(self.db()) { - if let Some(builder) = self - .context - .report_lint(&INVALID_DATACLASS, class.header_range(self.db())) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "`NamedTuple` class `{}` cannot be decorated with `@dataclass`", - class.name(self.db()), - )); - diagnostic.info( - "An exception will be raised when instantiating the class at runtime", - ); - } - } else if class.is_typed_dict(self.db()) { - if let Some(builder) = self - .context - .report_lint(&INVALID_DATACLASS, class.header_range(self.db())) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "`TypedDict` class `{}` cannot be decorated with `@dataclass`", - class.name(self.db()), - )); - diagnostic.info( - "An exception will often be raised when instantiating the class at runtime", - ); - } - } else if is_enum_class_by_inheritance(self.db(), class) { - if let Some(builder) = self - .context - .report_lint(&INVALID_DATACLASS, class.header_range(self.db())) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Enum class `{}` cannot be decorated with `@dataclass`", - class.name(self.db()), - )); - diagnostic - .info("Applying `@dataclass` to an enum is not supported at runtime"); - } - } else if is_protocol { - if let Some(builder) = self - .context - .report_lint(&INVALID_DATACLASS, class.header_range(self.db())) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Protocol class `{}` cannot be decorated with `@dataclass`", - class.name(self.db()), - )); - diagnostic.info( - "Protocols define abstract interfaces and cannot be instantiated", - ); - } - } - } - - let mut disjoint_bases = IncompatibleBases::default(); - let mut protocol_base_with_generic_context = None; - - // (5) Iterate through the class's explicit bases to check for various possible errors: - // - Check for inheritance from plain `Generic`, - // - Check for inheritance from a `@final` classes - // - If the class is a protocol class: check for inheritance from a non-protocol class - // - If the class is a NamedTuple class: check for multiple inheritance that isn't `Generic[]` - for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { - if class_kind == Some(CodeGeneratorKind::NamedTuple) - && !matches!( - base_class, - Type::SpecialForm(SpecialFormType::NamedTuple) - | Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)) - ) - { - if let Some(builder) = self - .context - .report_lint(&INVALID_NAMED_TUPLE, &class_node.bases()[i]) - { - builder.into_diagnostic(format_args!( - "NamedTuple class `{}` cannot use multiple inheritance except with `Generic[]`", - class.name(self.db()), - )); - } - } - - let base_class = match base_class { - Type::SpecialForm(SpecialFormType::Generic) => { - if let Some(builder) = self - .context - .report_lint(&INVALID_BASE, &class_node.bases()[i]) - { - // Unsubscripted `Generic` can appear in the MRO of many classes, - // but it is never valid as an explicit base class in user code. - builder.into_diagnostic("Cannot inherit from plain `Generic`"); - } - continue; - } - Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(new_context)) => { - let Some((previous_index, previous_context)) = - protocol_base_with_generic_context - else { - continue; - }; - let prior_node = &class_node.bases()[previous_index]; - let Some(builder) = - self.context.report_lint(&INVALID_GENERIC_CLASS, prior_node) - else { - continue; - }; - let mut diagnostic = builder.into_diagnostic( - "Cannot both inherit from subscripted `Protocol` \ - and subscripted `Generic`", - ); - if let ast::Expr::Subscript(prior_node) = prior_node - && new_context == previous_context - { - diagnostic.help("Remove the type parameters from the `Protocol` base"); - diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion( - TextRange::new(prior_node.value.end(), prior_node.end()), - ))); - } - continue; - } - // Note that unlike several of the other errors caught in this function, - // this does not lead to the class creation failing at runtime, - // but it is semantically invalid. - Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context)) => { - if let Some(type_params) = class_node.type_params.as_deref() { - let Some(builder) = self - .context - .report_lint(&INVALID_GENERIC_CLASS, &class_node.bases()[i]) - else { - continue; - }; - let mut diagnostic = builder.into_diagnostic( - "Cannot both inherit from subscripted `Protocol` \ - and use PEP 695 type variables", - ); - if let ast::Expr::Subscript(node) = &class_node.bases()[i] { - let source = source_text(self.db(), self.file()); - let type_params_range = TextRange::new( - type_params.start().saturating_add(TextSize::new(1)), - type_params.end().saturating_sub(TextSize::new(1)), - ); - if source[node.slice.range()] == source[type_params_range] { - diagnostic.help( - "Remove the type parameters from the `Protocol` base", - ); - diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion( - TextRange::new(node.value.end(), node.end()), - ))); - } - } - } else if protocol_base_with_generic_context.is_none() { - protocol_base_with_generic_context = Some((i, context)); - } - continue; - } - Type::ClassLiteral(class) => ClassType::NonGeneric(*class), - Type::GenericAlias(class) => ClassType::Generic(*class), - _ => continue, - }; - - if let Some(disjoint_base) = base_class.nearest_disjoint_base(self.db()) { - disjoint_bases.insert(disjoint_base, i, base_class.class_literal(self.db())); - } - - if is_protocol { - if !base_class.is_protocol(self.db()) - && !base_class.is_object(self.db()) - && let Some(builder) = self - .context - .report_lint(&INVALID_PROTOCOL, &class_node.bases()[i]) - { - builder.into_diagnostic(format_args!( - "Protocol class `{}` cannot inherit from non-protocol class `{}`", - class.name(self.db()), - base_class.name(self.db()), - )); - } - } else if class_kind == Some(CodeGeneratorKind::TypedDict) { - if !base_class.class_literal(self.db()).is_typed_dict(self.db()) - && let Some(builder) = self - .context - .report_lint(&INVALID_TYPED_DICT_HEADER, &class_node.bases()[i]) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "TypedDict class `{}` can only inherit from TypedDict classes", - class.name(self.db()), - )); - diagnostic.set_primary_message(format_args!( - "`{}` is not a `TypedDict` class", - base_class.name(self.db()) - )); - diagnostic.annotate( - Annotation::secondary( - base_class.class_literal(self.db()).header_span(self.db()), - ) - .message(format_args!( - "`{}` defined here", - base_class.name(self.db()) - )), - ); - } - } - - if base_class.is_final(self.db()) { - if let Some(builder) = self - .context - .report_lint(&SUBCLASS_OF_FINAL_CLASS, &class_node.bases()[i]) - { - builder.into_diagnostic(format_args!( - "Class `{}` cannot inherit from final class `{}`", - class.name(self.db()), - base_class.name(self.db()), - )); - } - } - - if let Some((base_class_literal, _)) = base_class.static_class_literal(self.db()) - && let (Some(base_is_frozen), Some(class_is_frozen)) = ( - base_class_literal.is_frozen_dataclass(self.db()), - class.is_frozen_dataclass(self.db()), - ) - && base_is_frozen != class_is_frozen - { - report_bad_frozen_dataclass_inheritance( - &self.context, - class, - class_node, - base_class_literal, - &class_node.bases()[i], - base_is_frozen, - ); - } - } - - // (6) Check for starred variable-length tuples that cannot be unpacked - let class_definition = self.index.expect_single_definition(class_node); - for base in class_node.bases() { - if let ast::Expr::Starred(starred) = base - && let starred_ty = - definition_expression_type(self.db(), class_definition, &starred.value) - && let Some(tuple_spec) = starred_ty.tuple_instance_spec(self.db()) - && !matches!(tuple_spec.as_ref(), Tuple::Fixed(_)) - { - report_unsupported_base(&self.context, base, starred_ty, class); - } - } - - // (7) Check that the class's MRO is resolvable - match class.try_mro(self.db(), None) { - Err(mro_error) => match mro_error.reason() { - StaticMroErrorKind::DuplicateBases(duplicates) => { - let base_nodes = class_node.bases(); - for duplicate in duplicates { - report_duplicate_bases(&self.context, class, duplicate, base_nodes); - } - } - StaticMroErrorKind::InvalidBases(bases) => { - let base_nodes = class_node.bases(); - for (index, base_ty) in bases { - report_invalid_or_unsupported_base( - &self.context, - &base_nodes[*index], - *base_ty, - class, - ); - } - } - StaticMroErrorKind::UnresolvableMro { - bases_list, - generic_index, - } => { - if let Some(builder) = self - .context - .report_lint(&INCONSISTENT_MRO, class.header_range(self.db())) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot create a consistent method resolution order (MRO) \ - for class `{}` with bases list `[{}]`", - class.name(self.db()), - bases_list - .iter() - .map(|base| base.display(self.db())) - .join(", ") - )); - if let Some(index) = *generic_index - && let [first_base, .., last_base] = class_node.bases() - { - let source = source_text(self.context.db(), self.context.file()); - let generic_base = &source[class_node.bases()[index].range()]; - diagnostic.help(format_args!( - "Move `{generic_base}` to the end of the bases list" - )); - let reordered_bases = class_node - .bases() - .iter() - .enumerate() - .filter(|(i, _)| *i != index) - .map(|(_, base)| &source[base.range()]) - .chain(std::iter::once(generic_base)) - .join(", "); - let fix = Fix::unsafe_edit(Edit::range_replacement( - reordered_bases, - TextRange::new(first_base.start(), last_base.end()), - )); - diagnostic.set_fix(fix); - } - } - } - StaticMroErrorKind::Pep695ClassWithGenericInheritance => { - if let Some(builder) = - self.context.report_lint(&INVALID_GENERIC_CLASS, class_node) - { - builder.into_diagnostic( - "Cannot both inherit from `typing.Generic` \ - and use PEP 695 type variables", - ); - } - } - StaticMroErrorKind::InheritanceCycle => { - if let Some(builder) = self - .context - .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) - { - builder.into_diagnostic(format_args!( - "Cyclic definition of `{}` (class cannot inherit from itself)", - class.name(self.db()) - )); - } - } - }, - Ok(_) => { - disjoint_bases.remove_redundant_entries(self.db()); - - if disjoint_bases.len() > 1 { - report_instance_layout_conflict( - &self.context, - class.header_range(self.db()), - Some(class_node.bases()), - &disjoint_bases, - ); - } - - // Check for inconsistent specializations of the same generic - // base class. This detects when different explicit bases - // contribute conflicting specializations of a common generic - // ancestor to the MRO. For example: - // - // class Grandparent(Generic[T1, T2]): ... - // class Parent(Grandparent[T1, T2]): ... - // class BadChild(Parent[T1, T2], Grandparent[T2, T1]): ... # Error - let explicit_bases = class.explicit_bases(self.db()); - let can_annotate_bases = || { - class_node.bases().len() == explicit_bases.len() - && !class_node.bases().iter().any(ast::Expr::is_starred_expr) - }; - - // Maps each generic ancestor's class literal to the first - // specialization seen and the index of the explicit base it - // came from. - let mut ancestor_specs = - FxHashMap::, (GenericAlias<'db>, usize)>::default(); - - 'outer: for (i, base) in explicit_bases.iter().enumerate() { - let base_class = match base { - Type::GenericAlias(c) => ClassType::Generic(*c), - Type::ClassLiteral(c) if c.generic_context(self.db()).is_none() => { - ClassType::NonGeneric(*c) - } - _ => continue, - }; - - for supercls in base_class.iter_mro(self.db()) { - let ClassBase::Class(ClassType::Generic(supercls_alias)) = supercls - else { - continue; - }; - let origin = supercls_alias.origin(self.db()); - - if let Some(&(earlier_alias, earlier_idx)) = ancestor_specs.get(&origin) - { - if earlier_idx != i - && earlier_alias - .specialization(self.db()) - .types(self.db()) - .iter() - .zip( - supercls_alias - .specialization(self.db()) - .types(self.db()), - ) - .any(|(t1, t2)| { - !t1.is_dynamic() && !t2.is_dynamic() && t1 != t2 - }) - { - let Some(builder) = self.context.report_lint( - &INVALID_GENERIC_CLASS, - class.header_range(self.db()), - ) else { - break 'outer; - }; - let mut diagnostic = builder.into_diagnostic(format_args!( - "Inconsistent type arguments for `{}` among class bases", - origin.name(self.db()) - )); - - let later_is_direct = matches!( - base, - Type::GenericAlias(a) - if a.origin(self.db()) == origin - ); - - if can_annotate_bases() { - diagnostic.annotate( - self.context - .secondary(&class_node.bases()[earlier_idx]) - .message(format_args!( - "Earlier class base inherits from `{}`", - earlier_alias.display(self.db()) - )), - ); - let later_annotation = - self.context.secondary(&class_node.bases()[i]); - diagnostic.annotate(if later_is_direct { - later_annotation.message(format_args!( - "Later class base is `{}`", - supercls_alias.display(self.db()) - )) - } else { - later_annotation.message(format_args!( - "Later class base inherits from `{}`", - supercls_alias.display(self.db()) - )) - }); - } else { - diagnostic.info(format_args!( - "Earlier class base inherits from `{}`", - earlier_alias.display(self.db()) - )); - if later_is_direct { - diagnostic.info(format_args!( - "Later class base is `{}`", - supercls_alias.display(self.db()) - )); - } else { - diagnostic.info(format_args!( - "Later class base inherits from `{}`", - supercls_alias.display(self.db()) - )); - } - } - diagnostic.set_concise_message(format_args!( - "Inconsistent type arguments: class cannot \ - inherit from both `{}` and `{}`", - supercls_alias.display(self.db()), - earlier_alias.display(self.db()) - )); - break 'outer; - } - } else if !supercls_alias - .specialization(self.db()) - .types(self.db()) - .iter() - .all(Type::is_dynamic) - { - ancestor_specs.insert(origin, (supercls_alias, i)); - } - } - } - } - } - - // (8) Check that @total_ordering has a valid ordering method in the MRO - if class.total_ordering(self.db()) && !class.has_ordering_method_in_mro(self.db(), None) - { - // Find the @total_ordering decorator to report the diagnostic at its location - if let Some(decorator) = class_node.decorator_list.iter().find(|decorator| { - self.expression_type(&decorator.expression) - .as_function_literal() - .is_some_and(|function| { - function.is_known(self.db(), KnownFunction::TotalOrdering) - }) - }) { - report_invalid_total_ordering( - &self.context, - ClassLiteral::Static(class), - decorator, - ); - } - } - - // (9) Check that the class's metaclass can be determined without error. - if let Err(metaclass_error) = class.try_metaclass(self.db()) { - match metaclass_error.reason() { - MetaclassErrorKind::Cycle => { - if let Some(builder) = self - .context - .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) - { - builder.into_diagnostic(format_args!( - "Cyclic definition of `{}`", - class.name(self.db()) - )); - } - } - MetaclassErrorKind::GenericMetaclass => { - if let Some(builder) = - self.context.report_lint(&INVALID_METACLASS, class_node) - { - builder.into_diagnostic("Generic metaclasses are not supported"); - } - } - MetaclassErrorKind::NotCallable(ty) => { - if let Some(builder) = - self.context.report_lint(&INVALID_METACLASS, class_node) - { - builder.into_diagnostic(format_args!( - "Metaclass type `{}` is not callable", - ty.display(self.db()) - )); - } - } - MetaclassErrorKind::PartlyNotCallable(ty) => { - if let Some(builder) = - self.context.report_lint(&INVALID_METACLASS, class_node) - { - builder.into_diagnostic(format_args!( - "Metaclass type `{}` is partly not callable", - ty.display(self.db()) - )); - } - } - MetaclassErrorKind::Conflict { - candidate1: - MetaclassCandidate { - metaclass: metaclass1, - explicit_metaclass_of: class1, - }, - candidate2: - MetaclassCandidate { - metaclass: metaclass2, - explicit_metaclass_of: class2, - }, - candidate1_is_base_class, - } => { - if *candidate1_is_base_class { - report_conflicting_metaclass_from_bases( - &self.context, - class_node.into(), - class.name(self.db()), - *metaclass1, - class1.name(self.db()), - *metaclass2, - class2.name(self.db()), - ); - } else if let Some(builder) = - self.context.report_lint(&CONFLICTING_METACLASS, class_node) - { - builder.into_diagnostic(format_args!( - "The metaclass of a derived class (`{class}`) \ - must be a subclass of the metaclasses of all its bases, \ - but `{metaclass_of_class}` (metaclass of `{class}`) \ - and `{metaclass_of_base}` (metaclass of base class `{base}`) \ - have no subclass relationship", - class = class.name(self.db()), - metaclass_of_class = metaclass1.name(self.db()), - metaclass_of_base = metaclass2.name(self.db()), - base = class2.name(self.db()), - )); - } - } - } - } - - // (10) Check that the class arguments matches the arguments of the - // base class `__init_subclass__` method. - if let Some(args) = class_node.arguments.as_deref() { - if class_kind == Some(CodeGeneratorKind::TypedDict) { - for keyword in &args.keywords { - match keyword.arg.as_deref() { - Some(arg_name @ ("total" | "closed")) => { - let passed_type = self.file_expression_type(&keyword.value); - if passed_type - .as_literal_value() - .is_none_or(|literal| !literal.is_bool()) - && let Some(builder) = - self.context.report_lint(&INVALID_ARGUMENT_TYPE, keyword) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `{arg_name}` \ - in `TypedDict` definition", - )); - diagnostic.set_primary_message(format_args!( - "Expected either `True` or `False`, got object of type `{}`", - passed_type.display(self.db()) - )); - } - } - Some("extra_items") => { - // TODO: validate that passed arguments here are annotation expressions - } - Some("metaclass") => { - if let Some(builder) = self - .context - .report_lint(&INVALID_TYPED_DICT_HEADER, keyword) - { - builder.into_diagnostic(format_args!( - "Custom metaclasses are not supported in `TypedDict` definitions", - )); - } - } - Some(other) => { - if let Some(builder) = - self.context.report_lint(&UNKNOWN_ARGUMENT, keyword) - { - builder.into_diagnostic(format_args!( - "Unknown keyword argument `{other}` \ - in `TypedDict` definition", - )); - } - } - None => { - if let Some(builder) = self - .context - .report_lint(&INVALID_TYPED_DICT_HEADER, keyword) - { - builder.into_diagnostic(format_args!( - "Keyword-variadic arguments are not supported in `TypedDict` definitions", - )); - } - } - } - } - } else { - let call_args: CallArguments = args - .keywords - .iter() - .filter_map(|keyword| match keyword.arg.as_ref() { - // We mimic the runtime behaviour and discard the metaclass argument - Some(name) if name.id.as_str() == "metaclass" => None, - Some(name) => { - let ty = self.file_expression_type(&keyword.value); - Some((Argument::Keyword(name.id.as_str()), Some(ty))) - } - None => { - let ty = self.file_expression_type(&keyword.value); - Some((Argument::Keywords, Some(ty))) - } - }) - .collect(); - - let init_subclass_type = class - .class_member_from_mro( - self.db(), - "__init_subclass__", - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - // skip(1) to skip the current class and only consider base classes. - class.iter_mro(self.db(), None).skip(1), - ) - .ignore_possibly_undefined(); - - if let Some(init_subclass) = init_subclass_type { - let call_args = call_args.with_self(Some(Type::from(class))); - if let Err(CallError(CallErrorKind::BindingError, bindings)) = - init_subclass.try_call(self.db(), &call_args) - { - bindings.report_diagnostics(&self.context, class_node.into()); - } - } - } - } - - // (11) If the class is generic, verify that its generic context does not violate any of - // the typevar scoping rules. - if let (Some(legacy), Some(inherited)) = ( - class.legacy_generic_context(self.db()), - class.inherited_legacy_generic_context(self.db()), - ) { - if !inherited.is_subset_of(self.db(), legacy) - && let Some(builder) = - self.context.report_lint(&INVALID_GENERIC_CLASS, class_node) - { - builder.into_diagnostic( - "`Generic` base class must include all type \ - variables used in other base classes", - ); - } - } - - if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS) { - if !class.has_pep_695_type_params(self.db()) - && let Some(generic_context) = class.legacy_generic_context(self.db()) - { - struct State<'db> { - typevar_with_default: TypeVarInstance<'db>, - invalid_later_tvars: Vec>, - } - - let mut state: Option> = None; - - for bound_typevar in generic_context.variables(self.db()) { - let typevar = bound_typevar.typevar(self.db()); - let has_default = typevar.default_type(self.db()).is_some(); - - if let Some(state) = state.as_mut() { - if !has_default { - state.invalid_later_tvars.push(typevar); - } - } else if has_default { - state = Some(State { - typevar_with_default: typevar, - invalid_later_tvars: vec![], - }); - } - } - - if let Some(state) = state - && !state.invalid_later_tvars.is_empty() - { - report_invalid_type_param_order( - &self.context, - class, - class_node, - state.typevar_with_default, - &state.invalid_later_tvars, - ); - } - } - - // Check that type variable defaults only reference type variables - // that precede them in the type parameter list. - if let Some(generic_context) = class - .pep695_generic_context(self.db()) - .or(class.legacy_generic_context(self.db())) - { - let db = self.db(); - let typevars = generic_context.variables(db).map(|btv| btv.typevar(db)); - - // `variables` should be fairly cheap to clone; it's just several cheap wrappers around - // a `std::slice::Iter` under the hood. - for (i, typevar) in typevars.clone().enumerate() { - let Some(default_ty) = typevar.default_type(db) else { - continue; - }; - - let first_bad_tvar = find_over_type(db, default_ty, false, |t| { - let tvar = match t { - Type::TypeVar(tvar) => tvar.typevar(db), - Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, - _ => return None, - }; - if !typevars.clone().take(i).contains(&tvar) { - Some(tvar) - } else { - None - } - }); - if let Some(bad_typevar) = first_bad_tvar { - let is_later_in_list = typevars.clone().skip(i).contains(&bad_typevar); - report_invalid_typevar_default_reference( - &self.context, - class, - typevar, - bad_typevar, - is_later_in_list, - ); - } - } - } - - let scope = class.body_scope(self.db()).scope(self.db()); - if let Some(parent) = scope.parent() { - // Check that the class's own type parameters don't shadow - // type variables from enclosing scopes (by name). - if let Some(generic_context) = class.generic_context(self.db()) { - for self_typevar in generic_context.variables(self.db()) { - let name = self_typevar.typevar(self.db()).name(self.db()); - for enclosing in - enclosing_generic_contexts(self.db(), self.index, parent) - { - if let Some(other_typevar) = - enclosing.binds_named_typevar(self.db(), name) - { - report_shadowed_type_variable( - &self.context, - name, - "class", - &class_node.name.id, - class.header_range(self.db()), - other_typevar, - ); - } - } - } - } - - // Check that the class's base classes don't reference type - // variables from enclosing scopes (by identity). - for base_typevar in class.typevars_referenced_in_bases(self.db()) { - let typevar = base_typevar.typevar(self.db()); - for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) { - if let Some(other_typevar) = enclosing.binds_typevar(self.db(), typevar) - { - report_shadowed_type_variable( - &self.context, - typevar.name(self.db()), - "class", - &class_node.name.id, - class.header_range(self.db()), - other_typevar, - ); - } - } - } - } - } - - // (12) Check that a dataclass does not have more than one `KW_ONLY` - // and that required fields are defined before default fields. - if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) = - CodeGeneratorKind::from_class(self.db(), class.into(), None) - { - let specialization = None; - - let mut kw_only_sentinel_fields = vec![]; - let mut required_after_default_field_names = vec![]; - let mut has_seen_default_field = false; - - for (name, field) in class.own_fields(self.db(), specialization, field_policy) { - if field.is_kw_only_sentinel(self.db()) { - kw_only_sentinel_fields.push(name); - continue; - } - - // Extract dataclass field properties - let FieldKind::Dataclass { - default_ty, - init, - kw_only, - .. - } = &field.kind - else { - continue; - }; - - // Fields with init=False or kw_only=true don't participate in ordering check - if !init || *kw_only == Some(true) { - continue; - } - - if default_ty.is_some() { - has_seen_default_field = true; - } else if has_seen_default_field { - required_after_default_field_names.push(name); - } - } - - if kw_only_sentinel_fields.len() > 1 { - // TODO: The fields should be displayed in a subdiagnostic. - if let Some(builder) = self - .context - .report_lint(&DUPLICATE_KW_ONLY, &class_node.name) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Dataclass has more than one field annotated with `KW_ONLY`" - )); - - diagnostic.info(format_args!( - "`KW_ONLY` fields: {}", - kw_only_sentinel_fields - .iter() - .map(|name| format!("`{name}`")) - .join(", ") - )); - } - } - - if !required_after_default_field_names.is_empty() { - // Report field ordering violations - let body_scope = class.body_scope(self.db()).file_scope_id(self.db()); - let use_def_map = self.index.use_def_map(body_scope); - let place_table = self.index.place_table(body_scope); - - for name in required_after_default_field_names { - let Some(symbol_id) = place_table.symbol_id(name.as_str()) else { - continue; - }; - for decl_with_constraints in - use_def_map.end_of_scope_symbol_declarations(symbol_id) - { - let Some(definition) = decl_with_constraints.declaration.definition() - else { - continue; - }; - if let DefinitionKind::AnnotatedAssignment(ann_assign) = - definition.kind(self.db()) - { - if let Some(builder) = self.context.report_lint( - &DATACLASS_FIELD_ORDER, - ann_assign.target(self.module()), - ) { - builder.into_diagnostic(format_args!( - "Required field `{name}` cannot be defined after fields with default values", - )); - } - break; - } - } - } - } - } - - // (13) Check for violations of the Liskov Substitution Principle, - // and for violations of other rules relating to invalid overrides of some sort. - overrides::check_class(&self.context, class); - - // (14) Check for unimplemented abstract methods on final classes. - self.check_final_class_abstract_methods(class, class_node); - - // (15) Check for Final-qualified declarations without a value. - self.check_class_final_without_value(class); - - if let Some(protocol) = class.into_protocol_class(self.db()) { - protocol.validate_members(&self.context); - } - - // (16) If it's a `TypedDict` class, check that it doesn't include any invalid - // statements: https://typing.python.org/en/latest/spec/typeddict.html#class-based-syntax - // - // The body of the class definition defines the items of the `TypedDict` type. It - // may also contain a docstring or pass statements (primarily to allow the creation - // of an empty `TypedDict`). No other statements are allowed, and type checkers - // should report an error if any are present. - if class.is_typed_dict(self.db()) { - for stmt in &class_node.body { - match stmt { - // Annotated assignments are allowed (that's the whole point), but they're - // not allowed to have a value. - ast::Stmt::AnnAssign(ann_assign) => { - if let Some(value) = &ann_assign.value - && let Some(builder) = self - .context - .report_lint(&INVALID_TYPED_DICT_STATEMENT, &**value) - { - builder.into_diagnostic("TypedDict item cannot have a value"); - } - - continue; - } - // Pass statements are allowed. - ast::Stmt::Pass(_) => continue, - ast::Stmt::Expr(expr) => { - // Docstrings are allowed. - if matches!(*expr.value, ast::Expr::StringLiteral(_)) { - continue; - } - // As a non-standard but common extension, we also interpret `...` as - // equivalent to `pass`. - if matches!(*expr.value, ast::Expr::EllipsisLiteral(_)) { - continue; - } - } - // Everything else is forbidden. - _ => {} - } - if let Some(builder) = self - .context - .report_lint(&INVALID_TYPED_DICT_STATEMENT, stmt) - { - if matches!(stmt, ast::Stmt::FunctionDef(_)) { - builder.into_diagnostic(format_args!( - "TypedDict class cannot have methods" - )); - } else { - let mut diagnostic = builder.into_diagnostic(format_args!( - "invalid statement in TypedDict class body" - )); - diagnostic.info( - "Only annotated declarations (`: `) are allowed.", - ); - } - } - } - } - - class.validate_members(&self.context); - } - } - - /// Check that a `@final` class does not have unimplemented abstract methods. - /// - /// A final class cannot be subclassed, so if it inherits abstract methods without - /// implementing them, those methods can never be implemented, making the class - /// effectively broken. - fn check_final_class_abstract_methods( - &self, - class: StaticClassLiteral<'db>, - class_node: &ast::StmtClassDef, - ) { - let db = self.db(); - - // Only check if the class is final. - if !class.is_final(db) { - return; - } - - // Exclude `Protocol` classes. It is possible to subtype a `Protocol` class - // without subclassing it, so an `@final` `Protocol` class with unimplemented abstract - // methods is not inherently broken in the same way as a non-`Protocol` final class - // with unimplemented abstract methods. - if class.is_protocol(db) { - return; - } - - let class_type = class.identity_specialization(db); - let abstract_methods = class_type.abstract_methods(db); - - // If there are no abstract methods, we're done. - let Some((first_method_name, abstract_method)) = abstract_methods.iter().next() else { - return; - }; - - let Some(builder) = self - .context - .report_lint(&ABSTRACT_METHOD_IN_FINAL_CLASS, &class_node.name) - else { - return; - }; - - let class_name = class.name(db); - - let mut diagnostic = builder.into_diagnostic(format_args!( - "Final class `{class_name}` has unimplemented abstract methods", - )); - - let num_abstract_methods = abstract_methods.len(); - - if num_abstract_methods == 1 { - diagnostic.set_concise_message(format_args!( - "Final class `{class_name}` has unimplemented abstract method \ - `{first_method_name}`", - )); - diagnostic.set_primary_message(format_args!("`{first_method_name}` is unimplemented")); - } else { - let verbose = db.verbose(); - let max_abstract_methods_to_print = if verbose { num_abstract_methods } else { 3 }; - let formatted_methods = - format_enumeration(abstract_methods.keys().take(max_abstract_methods_to_print)); - - if num_abstract_methods > max_abstract_methods_to_print { - diagnostic.set_primary_message(format_args!( - "{num_abstract_methods} abstract methods are unimplemented, \ - including {formatted_methods}", - )); - diagnostic.set_concise_message(format_args!( - "Final class `{class_name}` has {num_abstract_methods} unimplemented \ - abstract methods, including {formatted_methods}", - )); - diagnostic.info(format_args!( - "Use `--verbose` to see all {num_abstract_methods} \ - unimplemented abstract methods", - )); - } else { - diagnostic.set_concise_message(format_args!( - "Final class `{class_name}` has unimplemented \ - abstract methods {formatted_methods}", - )); - diagnostic.set_primary_message(format_args!( - "Abstract methods {formatted_methods} are unimplemented" - )); - } - } - - let AbstractMethod { - defining_class, - definition, - kind, - } = abstract_method; - - let module = parsed_module(db, definition.file(db)).load(db); - let span = Span::from(definition.focus_range(db, &module)); - let defining_class_name = defining_class.name(db); - - let mut secondary_annotation = Annotation::secondary(span); - secondary_annotation = if defining_class.class_literal(db) == ClassLiteral::Static(class) { - secondary_annotation.message(format_args!("`{first_method_name}` declared as abstract")) - } else { - secondary_annotation.message(format_args!( - "`{first_method_name}` declared as abstract on superclass `{defining_class_name}`", - )) - }; - diagnostic.annotate(secondary_annotation); - - if !kind.is_explicit() { - let mut sub = SubDiagnostic::new( - SubDiagnosticSeverity::Info, - format_args!( - "`{defining_class_name}.{first_method_name}` is implicitly abstract \ - because `{defining_class_name}` is a `Protocol` class \ - and `{first_method_name}` lacks an implementation", - ), - ); - sub.annotate( - Annotation::secondary(defining_class.definition_span(db)) - .message(format_args!("`{defining_class_name}` declared here")), - ); - diagnostic.sub(sub); - - // If the implicitly abstract method is defined in first-party code - // and the return type is assignable to `None`, they may not have intended - // for it to be implicitly abstract; add a clarificatory note: - if kind.is_implicit_due_to_stub_body() && db.should_check_file(definition.file(db)) { - let function_type_as_callable = infer_definition_types(db, *definition) - .binding_type(*definition) - .try_upcast_to_callable(db); - - if let Some(callables) = function_type_as_callable - && Type::function_like_callable( - db, - Signature::new(Parameters::gradual_form(), Type::none(db)), - ) - .is_assignable_to(db, callables.into_type(db)) - { - diagnostic.help(format_args!( - "Change the body of `{first_method_name}` to `return` \ - or `return None` if it was not intended to be abstract" - )); - } - } - } - } - - /// Check for `Final`-qualified declarations in module/function scopes that are never - /// assigned a value. Class body scopes are handled separately in - /// [`Self::check_class_final_without_value`]. - fn check_final_without_value(&self) { - // In stub files, bare declarations without values are normal. - if self.in_stub() { - return; - } - - // Class body scopes are handled separately in check_class_final_without_value, - // which has access to the class literal to handle special cases (e.g. dataclasses). - let db = self.db(); - let file_scope_id = self.scope().file_scope_id(db); - if self.index.scope(file_scope_id).kind().is_class() { - return; - } - - let use_def = self.index.use_def_map(file_scope_id); - let place_table = self.index.place_table(file_scope_id); - - for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations); - let first_declaration = result.first_declaration; - let (place_and_quals, _) = result.into_place_and_conflicting_declarations(); - - if !place_and_quals.qualifiers.contains(TypeQualifiers::FINAL) { - continue; - } - - // Imports inherit the `Final` qualifier from the source module, but the - // import itself provides the value. Don't flag imported `Final` symbols, - // even if a later `del` removes the binding at end-of-scope. - if first_declaration.is_some_and(|decl| decl.kind(db).is_import()) { - continue; - } - - // Check if the symbol has any bindings in the current scope. - let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let binding_place = place_from_bindings(db, bindings); - - if !binding_place.place.is_undefined() { - continue; - } - - let place = place_table.place(symbol_id); - if let Some(first_decl) = first_declaration { - if let Some(builder) = self.context.report_lint( - &FINAL_WITHOUT_VALUE, - first_decl.full_range(db, self.module()), - ) { - builder.into_diagnostic(format_args!( - "`Final` symbol `{place}` is not assigned a value" - )); - } - } - } - } - - /// Check for `Final`-qualified declarations in a class body scope that are never - /// assigned a value. - fn check_class_final_without_value(&self, class: StaticClassLiteral<'db>) { - // In stub files, bare declarations without values are normal. - if self.in_stub() { - return; - } - - let db = self.db(); - let body_scope = class.body_scope(db); - let body_scope_id = body_scope.file_scope_id(db); - let use_def = self.index.use_def_map(body_scope_id); - let place_table = self.index.place_table(body_scope_id); - - // In dataclasses (and similar code-generated classes), Final fields without - // defaults are initialized by the synthesized __init__, so they are valid. - if CodeGeneratorKind::from_class(db, class.into(), None).is_some() { - return; - } - - for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations); - let first_declaration = result.first_declaration; - let (place_and_quals, _) = result.into_place_and_conflicting_declarations(); - - if !place_and_quals.qualifiers.contains(TypeQualifiers::FINAL) { - continue; - } - - // Check if the symbol has any bindings at class level. - let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let binding_place = place_from_bindings(db, bindings); - - if !binding_place.place.is_undefined() { - continue; - } - - // Per the typing spec, a `Final` attribute declared in a class body without a - // value must be initialized in `__init__`. Assignments in other methods don't count. - let symbol = place_table.symbol(symbol_id); - if self.has_binding_in_init(body_scope, symbol.name().as_str()) { - continue; - } - - let place = place_table.place(symbol_id); - if let Some(first_decl) = first_declaration { - if let Some(builder) = self.context.report_lint( - &FINAL_WITHOUT_VALUE, - first_decl.full_range(db, self.module()), - ) { - builder.into_diagnostic(format_args!( - "`Final` symbol `{place}` is not assigned a value" - )); - } - } - } - } - - /// Returns `true` if `name` has any attribute assignment (`self. = ...`) in an - /// `__init__` method of the class whose body scope is `class_body_scope`. - fn has_binding_in_init(&self, class_body_scope: ScopeId<'db>, name: &str) -> bool { - let db = self.db(); - attribute_assignments(db, class_body_scope, name).any(|(bindings, scope_id)| { - let is_init = self - .index - .scope(scope_id) - .node() - .as_function() - .is_some_and(|f| f.node(self.module()).name.id == "__init__"); - is_init - && bindings - .into_iter() - .any(|b| b.binding.definition().is_some()) - }) - } - - /// Check the overloaded functions in this scope. - /// - /// This only checks the overloaded functions that are: - /// 1. Visible publicly at the end of this scope - /// 2. Or, defined and called in this scope - /// - /// For (1), this has the consequence of not checking an overloaded function that is being - /// shadowed by another function with the same name in this scope. - fn check_overloaded_functions(&mut self, scope: &NodeWithScopeKind) { - // Collect all the unique overloaded function places in this scope. This requires a set - // because an overloaded function uses the same place for each of the overloads and the - // implementation. - let overloaded_function_places: FxIndexSet<_> = self - .declarations - .iter() - .filter_map(|(definition, ty)| { - // Filter out function literals that result from anything other than a function - // definition e.g., imports which would create a cross-module AST dependency. - if !matches!(definition.kind(self.db()), DefinitionKind::Function(_)) { - return None; - } - let function = ty.inner_type().as_function_literal()?; - if function.has_known_decorator(self.db(), FunctionDecorators::OVERLOAD) { - Some(definition.place(self.db())) - } else { - None - } - }) - .collect(); - - let use_def = self - .index - .use_def_map(self.scope().file_scope_id(self.db())); - - let mut public_functions = FxIndexSet::default(); - - for place in overloaded_function_places { - if let Place::Defined(DefinedPlace { - ty: Type::FunctionLiteral(function), - definedness: Definedness::AlwaysDefined, - .. - }) = place_from_bindings( - self.db(), - use_def.end_of_scope_symbol_bindings(place.as_symbol().unwrap()), - ) - .place - { - if function.file(self.db()) != self.file() { - // If the function is not in this file, we don't need to check it. - // https://github.com/astral-sh/ruff/pull/17609#issuecomment-2839445740 - continue; - } - - // Extend the functions that we need to check with the publicly visible overloaded - // function. This is always going to be either the implementation or the last - // overload if the implementation doesn't exists. - public_functions.insert(function); - } - } - - for function in self.called_functions.union(&public_functions) { - let (overloads, implementation) = function.overloads_and_implementation(self.db()); - if overloads.is_empty() { - continue; - } - - // Check that the overloaded function has at least two overloads - if let [single_overload] = overloads { - let function_node = single_overload.node(self.db(), self.file(), self.module()); - if let Some(builder) = self - .context - .report_lint(&INVALID_OVERLOAD, &function_node.name) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Overloaded function `{}` requires at least two overloads", - &function_node.name - )); - diagnostic.set_primary_message("Only one overload defined here"); - } + NodeWithScopeKind::GeneratorExpression(generator) => { + self.infer_generator_expression_scope(generator.node(self.module())); } + } - // Check that the overloaded function has an implementation. Overload definitions - // within stub files, protocols, and on abstract methods within abstract base classes - // are exempt from this check. - if implementation.is_none() && !self.in_stub() { - let mut implementation_required = true; - - if function - .iter_overloads_and_implementation(self.db()) - .all(|f| { - f.body_scope(self.db()) - .scope(self.db()) - .in_type_checking_block() - }) - { - implementation_required = false; - } else if let NodeWithScopeKind::Class(class_node_ref) = scope { - let class = binding_type( - self.db(), - self.index - .expect_single_definition(class_node_ref.node(self.module())), - ) - .expect_class_literal(); + // Infer deferred types for all definitions. + let deferred_definitions: Vec<_> = std::mem::take(&mut self.deferred).into_iter().collect(); + for definition in &deferred_definitions { + self.extend_definition(infer_deferred_types(self.db(), *definition)); + } - if class.is_protocol(self.db()) - || (Type::ClassLiteral(class) - .is_subtype_of(self.db(), KnownClass::ABCMeta.to_instance(self.db())) - && overloads.iter().all(|overload| { - overload.has_known_decorator( - self.db(), - FunctionDecorators::ABSTRACT_METHOD, - ) - })) - { - implementation_required = false; - } - } + assert!( + self.deferred.is_empty(), + "Inferring deferred types should not add more deferred definitions" + ); - if implementation_required { - let function_node = overloads[0].node(self.db(), self.file(), self.module()); - if let Some(builder) = self - .context - .report_lint(&INVALID_OVERLOAD, &function_node.name) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Overloads for function `{}` must be followed by a non-`@overload`-decorated implementation function", - &function_node.name - )); - diagnostic.info(format_args!( - "Attempting to call `{}` will raise `TypeError` at runtime", - &function_node.name - )); - diagnostic.info( - "Overloaded functions without implementations are only permitted:", + if self.db().should_check_file(self.file()) { + let mut seen_overloaded_places = FxHashSet::default(); + let mut seen_public_functions = FxHashSet::default(); + + for (definition, ty_and_quals) in &self.declarations { + let ty = ty_and_quals.inner_type(); + match definition.kind(self.db()) { + DefinitionKind::Function(function) => { + deferred::function::check_function_definition( + &self.context, + *definition, + &|expr| self.file_expression_type(expr), ); - diagnostic.info(" - in stub files"); - diagnostic.info(" - in `if TYPE_CHECKING` blocks"); - diagnostic.info(" - as methods on protocol classes"); - diagnostic.info( - " - or as `@abstractmethod`-decorated methods on abstract classes", + deferred::overloaded_function::check_overloaded_function( + &self.context, + ty, + *definition, + self.scope.scope(self.db()).node(), + self.index, + &mut seen_overloaded_places, + &mut seen_public_functions, ); - diagnostic.info( - "See https://docs.python.org/3/library/typing.html#typing.overload \ - for more details", + deferred::typeguard::check_type_guard_definition( + &self.context, + ty, + function.node(self.module()), + self.index, ); } - } - } - - for (decorator, name) in [ - (FunctionDecorators::CLASSMETHOD, "classmethod"), - (FunctionDecorators::STATICMETHOD, "staticmethod"), - ] { - let mut decorator_present = false; - let mut decorator_missing = vec![]; - - for function in overloads.iter().chain(implementation.as_ref()) { - if function.has_known_decorator(self.db(), decorator) { - decorator_present = true; - } else { - decorator_missing.push(function); - } - } - - if !decorator_present { - // Both overloads and implementation does not have the decorator - continue; - } - if decorator_missing.is_empty() { - // All overloads and implementation have the decorator - continue; - } - - let function_node = function.node(self.db(), self.file(), self.module()); - if let Some(builder) = self - .context - .report_lint(&INVALID_OVERLOAD, &function_node.name) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Overloaded function `{}` does not use the `@{name}` decorator \ - consistently", - &function_node.name - )); - for function in decorator_missing { - diagnostic.annotate( - self.context - .secondary(function.focus_range(self.db(), self.module())) - .message(format_args!("Missing here")), + DefinitionKind::Class(class_node) => { + deferred::static_class::check_static_class_definitions( + &self.context, + ty, + class_node.node(self.module()), + self.index, + &|expr| self.file_expression_type(expr), ); } + _ => {} } } - for (function, decorator) in [ - (KnownFunction::Final, FunctionDecorators::FINAL), - (KnownFunction::Override, FunctionDecorators::OVERRIDE), - ] { - if let Some(implementation) = implementation { - for overload in overloads { - if !overload.has_known_decorator(self.db(), decorator) { - continue; - } - let function_node = overload.node(self.db(), self.file(), self.module()); - let Some(builder) = self - .context - .report_lint(&INVALID_OVERLOAD, &function_node.name) - else { - continue; - }; - let mut diagnostic = builder.into_diagnostic(format_args!( - "`@{name}` decorator should be applied only to the \ - overload implementation", - name = function.name() - )); - if let Some(decorator) = - overload.find_known_decorator_span(self.db(), function) - { - diagnostic.annotate(Annotation::secondary(decorator)); - } - diagnostic.annotate( - self.context - .secondary(implementation.focus_range(self.db(), self.module())) - .message(format_args!("Implementation defined here")), - ); - } - } else { - let mut overloads = overloads.iter(); - let Some(first_overload) = overloads.next() else { - continue; - }; - for overload in overloads { - if !overload.has_known_decorator(self.db(), decorator) { - continue; - } - let function_node = overload.node(self.db(), self.file(), self.module()); - let Some(builder) = self - .context - .report_lint(&INVALID_OVERLOAD, &function_node.name) - else { - continue; - }; - let mut diagnostic = builder.into_diagnostic(format_args!( - "`@{name}` decorator should be applied only to the \ - first overload", - name = function.name() - )); - if let Some(decorator) = - overload.find_known_decorator_span(self.db(), function) - { - diagnostic.annotate(Annotation::secondary(decorator)); - } - diagnostic.annotate( - self.context - .secondary(first_overload.focus_range(self.db(), self.module())) - .message(format_args!("First overload defined here")), - ); - } - } + for definition in &deferred_definitions { + deferred::dynamic_class::check_dynamic_class_definition(&self.context, *definition); } - } - } - - /// Check that all type guard function definitions have at least one positional parameter - /// (in addition to `self`/`cls` for methods), and for `TypeIs`, that the narrowed type is - /// assignable to the declared type of that parameter. - fn check_type_guard_definitions(&mut self) { - for (definition, ty) in &self.declarations { - // Only check actual function definitions, not imports. - let DefinitionKind::Function(function_ref) = definition.kind(self.db()) else { - continue; - }; - - let Some(function) = ty.inner_type().as_function_literal() else { - continue; - }; - - for overload in function.iter_overloads_and_implementation(self.db()) { - let signature = overload.signature(self.db()); - let return_ty = signature.return_ty; - - // Check if this is a `TypeIs` or `TypeGuard` return type. - let (type_guard_form_name, narrowed_type) = match return_ty { - Type::TypeIs(type_is) => ("TypeIs", Some(type_is.return_type(self.db()))), - Type::TypeGuard(_) => ("TypeGuard", None), - _ => continue, - }; - - let function_node = function_ref.node(self.module()); - - // The return type annotation must exist since we matched `TypeIs`/`TypeGuard`. - let Some(returns_expr) = function_node.returns.as_deref() else { - continue; - }; - - // Check if this is a non-static method (first parameter is implicit `self`/`cls`). - let is_method = self - .index - .class_definition_of_method( - overload.body_scope(self.db()).file_scope_id(self.db()), - ) - .is_some(); - let has_implicit_receiver = is_method && !overload.is_staticmethod(self.db()); - - // Find the first positional parameter to narrow (skip implicit `self`/`cls`). - let positional_params: Vec<_> = signature.parameters().positional().collect(); - let first_narrowed_param_index = usize::from(has_implicit_receiver); - let first_narrowed_param = positional_params.get(first_narrowed_param_index); - - let Some(first_narrowed_param) = first_narrowed_param else { - if let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr) - { - builder.into_diagnostic(format_args!( - "`{type_guard_form_name}` function must have a parameter to narrow" - )); - } - continue; - }; - // For `TypeIs`, check that the narrowed type is assignable to the parameter type. - if let Some(narrowed_ty) = narrowed_type { - let param_ty = first_narrowed_param.annotated_type(); - if !narrowed_ty.is_assignable_to(self.db(), param_ty) { - if let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr) - { - builder.into_diagnostic(format_args!( - "Narrowed type `{narrowed}` is not assignable \ - to the declared parameter type `{param}`", - narrowed = narrowed_ty.display(self.db()), - param = param_ty.display(self.db()) - )); - } - } - } + for function in &self.called_functions { + deferred::overloaded_function::check_overloaded_function( + &self.context, + Type::FunctionLiteral(*function), + function.definition(self.db()), + self.scope.scope(self.db()).node(), + self.index, + &mut seen_overloaded_places, + &mut seen_public_functions, + ); } + + deferred::final_variable::check_final_without_value(&self.context, self.index); } } @@ -8187,203 +6192,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.validate_dynamic_type_bases(bases_arg, &bases, name); } - /// Iterate over all dynamic class definitions (created using `type()` calls) to check that - /// the definition will not cause an exception to be raised at runtime. This needs to be done - /// after deferred inference completes, since bases may contain forward references. - fn check_dynamic_class_definitions(&mut self, deferred_definitions: &[Definition<'db>]) { - let db = self.db(); - let module = self.module(); - - for definition in deferred_definitions { - // Only check assignment definitions (`type()` calls). - let DefinitionKind::Assignment(assignment) = definition.kind(db) else { - continue; - }; - - // Get the binding type for this definition. - let ty = binding_type(db, *definition); - - // Check if it's a dynamic class with a Definition anchor. - let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = ty else { - continue; - }; - - // Only check classes with Definition anchors (i.e., assigned `type()` calls). - // Dangling `type()` calls are validated eagerly during inference. - let DynamicClassAnchor::Definition(_) = dynamic_class.anchor(db) else { - continue; - }; - - let value = assignment.value(module); - let Some(call_expr) = value.as_call_expr() else { - continue; - }; - - self.check_dynamic_class_definition(dynamic_class, call_expr); - } - } - - /// Report MRO errors for a dynamic class. - /// - /// Returns `true` if the MRO is valid, `false` if there were errors. - fn report_dynamic_mro_errors( - &mut self, - dynamic_class: DynamicClassLiteral<'db>, - call_expr: &ast::ExprCall, - bases: &ast::Expr, - ) -> bool { - let db = self.db(); - let Err(error) = dynamic_class.try_mro(db) else { - return true; - }; - - let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); - - match error.reason() { - DynamicMroErrorKind::InvalidBases(invalid_bases) => { - for (idx, base_type) in invalid_bases { - // Check if the type is "type-like" (e.g., `type[Base]`). - let instance_of_type = KnownClass::Type.to_instance(db); - - // Determine the diagnostic node; prefer specific base expr, fall back to bases. - let specific_base = bases_tuple_elts.and_then(|elts| elts.get(*idx)); - let diagnostic_range = specific_base - .map(ast::Expr::range) - .unwrap_or_else(|| bases.range()); - - if base_type.is_assignable_to(db, instance_of_type) { - if let Some(builder) = self - .context - .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_range) - { - let mut diagnostic = builder.into_diagnostic("Unsupported class base"); - diagnostic.set_primary_message(format_args!( - "Has type `{}`", - base_type.display(db) - )); - diagnostic.info(format_args!( - "ty cannot determine a MRO for class `{}` due to this base", - dynamic_class.name(db) - )); - diagnostic - .info("Only class objects or `Any` are supported as class bases"); - } - } else if let Some(builder) = - self.context.report_lint(&INVALID_BASE, diagnostic_range) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid class base with type `{}`", - base_type.display(db) - )); - if specific_base.is_none() { - diagnostic - .info(format_args!("Element {} of the tuple is invalid", idx + 1)); - } - } - } - } - DynamicMroErrorKind::InheritanceCycle => { - if let Some(builder) = self - .context - .report_lint(&CYCLIC_CLASS_DEFINITION, call_expr) - { - builder.into_diagnostic(format_args!( - "Cyclic definition of `{}`", - dynamic_class.name(db) - )); - } - } - DynamicMroErrorKind::DuplicateBases(duplicates) => { - if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) { - builder.into_diagnostic(format_args!( - "Duplicate base class{maybe_s} {dupes} in class `{class}`", - maybe_s = if duplicates.len() == 1 { "" } else { "es" }, - dupes = duplicates - .iter() - .map(|base: &ClassBase<'_>| base.display(db)) - .join(", "), - class = dynamic_class.name(db), - )); - } - } - DynamicMroErrorKind::UnresolvableMro => { - if let Some(builder) = self.context.report_lint(&INCONSISTENT_MRO, call_expr) { - builder.into_diagnostic(format_args!( - "Cannot create a consistent method resolution order (MRO) \ - for class `{}` with bases `[{}]`", - dynamic_class.name(db), - dynamic_class - .explicit_bases(db) - .iter() - .map(|base| base.display(db)) - .join(", ") - )); - } - } - } - - false - } - - /// Check a single dynamic class definition for MRO and metaclass errors. - fn check_dynamic_class_definition( - &mut self, - dynamic_class: DynamicClassLiteral<'db>, - call_expr: &ast::ExprCall, - ) { - let db = self.db(); - - // A valid 3-argument type() call must have a `bases` argument. - let Some(bases) = call_expr.arguments.args.get(1) else { - return; - }; - - // Check for MRO errors. - if self.report_dynamic_mro_errors(dynamic_class, call_expr, bases) { - // MRO succeeded, check for instance-layout-conflict. - let mut disjoint_bases = IncompatibleBases::default(); - let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); - - for (idx, base_type) in dynamic_class.explicit_bases(db).iter().enumerate() { - // Convert to ClassType to access nearest_disjoint_base. - if let Some(class_type) = base_type.to_class_type(db) { - if let Some(disjoint_base) = class_type.nearest_disjoint_base(db) { - disjoint_bases.insert(disjoint_base, idx, class_type.class_literal(db)); - } - } - } - - disjoint_bases.remove_redundant_entries(db); - if disjoint_bases.len() > 1 { - report_instance_layout_conflict( - &self.context, - dynamic_class.header_range(db), - bases_tuple_elts, - &disjoint_bases, - ); - } - } - - // Check for metaclass conflicts. - if let Err(DynamicMetaclassConflict { - metaclass1, - base1, - metaclass2, - base2, - }) = dynamic_class.try_metaclass(db) - { - report_conflicting_metaclass_from_bases( - &self.context, - call_expr.into(), - dynamic_class.name(db), - metaclass1, - base1.display(db), - metaclass2, - base2.display(db), - ); - } - } - /// Infer a call to `builtins.type()`. /// /// `builtins.type` has two overloads: a single-argument overload (e.g. `type("foo")`, @@ -8638,7 +6446,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.validate_dynamic_type_bases(bases_arg, explicit_bases, &name); // Check for MRO errors. - if self.report_dynamic_mro_errors(dynamic_class, call_expr, bases_arg) { + if report_dynamic_mro_errors(&self.context, dynamic_class, call_expr, bases_arg) { // MRO succeeded, check for instance-layout-conflict. disjoint_bases.remove_redundant_entries(db); if disjoint_bases.len() > 1 { @@ -15802,3 +13610,98 @@ enum BoundOrConstraintsNodes<'ast> { Bound(&'ast ast::Expr), Constraints(&'ast [ast::Expr]), } + +/// Report MRO errors for a dynamic class. +/// +/// Returns `true` if the MRO is valid, `false` if there were errors. +pub(super) fn report_dynamic_mro_errors<'db>( + context: &InferContext<'db, '_>, + dynamic_class: DynamicClassLiteral<'db>, + call_expr: &ast::ExprCall, + bases: &ast::Expr, +) -> bool { + let db = context.db(); + let Err(error) = dynamic_class.try_mro(db) else { + return true; + }; + + let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); + + match error.reason() { + DynamicMroErrorKind::InvalidBases(invalid_bases) => { + for (idx, base_type) in invalid_bases { + // Check if the type is "type-like" (e.g., `type[Base]`). + let instance_of_type = KnownClass::Type.to_instance(db); + + // Determine the diagnostic node; prefer specific base expr, fall back to bases. + let specific_base = bases_tuple_elts.and_then(|elts| elts.get(*idx)); + let diagnostic_range = specific_base + .map(ast::Expr::range) + .unwrap_or_else(|| bases.range()); + + if base_type.is_assignable_to(db, instance_of_type) { + if let Some(builder) = + context.report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_range) + { + let mut diagnostic = builder.into_diagnostic("Unsupported class base"); + diagnostic.set_primary_message(format_args!( + "Has type `{}`", + base_type.display(db) + )); + diagnostic.info(format_args!( + "ty cannot determine a MRO for class `{}` due to this base", + dynamic_class.name(db) + )); + diagnostic.info("Only class objects or `Any` are supported as class bases"); + } + } else if let Some(builder) = context.report_lint(&INVALID_BASE, diagnostic_range) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid class base with type `{}`", + base_type.display(db) + )); + if specific_base.is_none() { + diagnostic + .info(format_args!("Element {} of the tuple is invalid", idx + 1)); + } + } + } + } + DynamicMroErrorKind::InheritanceCycle => { + if let Some(builder) = context.report_lint(&CYCLIC_CLASS_DEFINITION, call_expr) { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}`", + dynamic_class.name(db) + )); + } + } + DynamicMroErrorKind::DuplicateBases(duplicates) => { + if let Some(builder) = context.report_lint(&DUPLICATE_BASE, call_expr) { + builder.into_diagnostic(format_args!( + "Duplicate base class{maybe_s} {dupes} in class `{class}`", + maybe_s = if duplicates.len() == 1 { "" } else { "es" }, + dupes = duplicates + .iter() + .map(|base: &ClassBase<'_>| base.display(db)) + .join(", "), + class = dynamic_class.name(db), + )); + } + } + DynamicMroErrorKind::UnresolvableMro => { + if let Some(builder) = context.report_lint(&INCONSISTENT_MRO, call_expr) { + builder.into_diagnostic(format_args!( + "Cannot create a consistent method resolution order (MRO) \ + for class `{}` with bases `[{}]`", + dynamic_class.name(db), + dynamic_class + .explicit_bases(db) + .iter() + .map(|base| base.display(db)) + .join(", ") + )); + } + } + } + + false +} diff --git a/crates/ty_python_semantic/src/types/infer/deferred/dynamic_class.rs b/crates/ty_python_semantic/src/types/infer/deferred/dynamic_class.rs new file mode 100644 index 0000000000000..7c1eb3dddfa92 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/deferred/dynamic_class.rs @@ -0,0 +1,95 @@ +use crate::{ + semantic_index::definition::{Definition, DefinitionKind}, + types::{ + ClassLiteral, Type, binding_type, + class::{DynamicClassAnchor, DynamicMetaclassConflict}, + context::InferContext, + diagnostic::{ + IncompatibleBases, report_conflicting_metaclass_from_bases, + report_instance_layout_conflict, + }, + infer::builder::report_dynamic_mro_errors, + }, +}; + +/// Iterate over all dynamic class definitions (created using `type()` calls) to check that +/// the definition will not cause an exception to be raised at runtime. This needs to be done +/// after deferred inference completes, since bases may contain forward references. +pub(crate) fn check_dynamic_class_definition<'db>( + context: &InferContext<'db, '_>, + definition: Definition<'db>, +) { + let db = context.db(); + + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + return; + }; + + let ty = binding_type(db, definition); + + // Check if it's a dynamic class with a Definition anchor. + let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = ty else { + return; + }; + + // Only check classes with Definition anchors (i.e., assigned `type()` calls). + // Dangling `type()` calls are validated eagerly during inference. + let DynamicClassAnchor::Definition(_) = dynamic_class.anchor(db) else { + return; + }; + + let value = assignment.value(context.module()); + let Some(call_expr) = value.as_call_expr() else { + return; + }; + + // A valid 3-argument type() call must have a `bases` argument. + let Some(bases) = call_expr.arguments.args.get(1) else { + return; + }; + + // Check for MRO errors. + if report_dynamic_mro_errors(context, dynamic_class, call_expr, bases) { + // MRO succeeded, check for instance-layout-conflict. + let mut disjoint_bases = IncompatibleBases::default(); + let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); + + for (idx, base_type) in dynamic_class.explicit_bases(db).iter().enumerate() { + // Convert to ClassType to access nearest_disjoint_base. + if let Some(class_type) = base_type.to_class_type(db) { + if let Some(disjoint_base) = class_type.nearest_disjoint_base(db) { + disjoint_bases.insert(disjoint_base, idx, class_type.class_literal(db)); + } + } + } + + disjoint_bases.remove_redundant_entries(db); + if disjoint_bases.len() > 1 { + report_instance_layout_conflict( + context, + dynamic_class.header_range(db), + bases_tuple_elts, + &disjoint_bases, + ); + } + } + + // Check for metaclass conflicts. + if let Err(DynamicMetaclassConflict { + metaclass1, + base1, + metaclass2, + base2, + }) = dynamic_class.try_metaclass(db) + { + report_conflicting_metaclass_from_bases( + context, + call_expr.into(), + dynamic_class.name(db), + metaclass1, + base1.display(db), + metaclass2, + base2.display(db), + ); + } +} diff --git a/crates/ty_python_semantic/src/types/infer/deferred/final_variable.rs b/crates/ty_python_semantic/src/types/infer/deferred/final_variable.rs new file mode 100644 index 0000000000000..bb55c283eb3a7 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/deferred/final_variable.rs @@ -0,0 +1,67 @@ +use crate::{ + TypeQualifiers, + place::{place_from_bindings, place_from_declarations}, + semantic_index::SemanticIndex, + types::{context::InferContext, diagnostic::FINAL_WITHOUT_VALUE}, +}; + +/// Check for `Final`-qualified declarations in module/function scopes that are never +/// assigned a value. Class body scopes are handled separately in +/// `check_class_final_without_value`. +pub(crate) fn check_final_without_value<'db>( + context: &InferContext<'db, '_>, + index: &SemanticIndex<'db>, +) { + // In stub files, bare declarations without values are normal. + if context.in_stub() { + return; + } + + // Class body scopes are handled separately in check_class_final_without_value, + // which has access to the class literal to handle special cases (e.g. dataclasses). + let db = context.db(); + let file_scope_id = context.scope().file_scope_id(db); + if index.scope(file_scope_id).kind().is_class() { + return; + } + + let use_def = index.use_def_map(file_scope_id); + let place_table = index.place_table(file_scope_id); + + for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { + let result = place_from_declarations(db, declarations); + let first_declaration = result.first_declaration; + let (place_and_quals, _) = result.into_place_and_conflicting_declarations(); + + if !place_and_quals.qualifiers.contains(TypeQualifiers::FINAL) { + continue; + } + + // Imports inherit the `Final` qualifier from the source module, but the + // import itself provides the value. Don't flag imported `Final` symbols, + // even if a later `del` removes the binding at end-of-scope. + if first_declaration.is_some_and(|decl| decl.kind(db).is_import()) { + continue; + } + + // Check if the symbol has any bindings in the current scope. + let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); + let binding_place = place_from_bindings(db, bindings); + + if !binding_place.place.is_undefined() { + continue; + } + + let place = place_table.place(symbol_id); + if let Some(first_decl) = first_declaration + && let Some(builder) = context.report_lint( + &FINAL_WITHOUT_VALUE, + first_decl.full_range(db, context.module()), + ) + { + builder.into_diagnostic(format_args!( + "`Final` symbol `{place}` is not assigned a value" + )); + } + } +} diff --git a/crates/ty_python_semantic/src/types/infer/deferred/function.rs b/crates/ty_python_semantic/src/types/infer/deferred/function.rs new file mode 100644 index 0000000000000..b7b6edc160d7c --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/deferred/function.rs @@ -0,0 +1,341 @@ +use crate::{ + diagnostic::format_enumeration, + semantic_index::definition::Definition, + types::{ + KnownInstanceType, Signature, Type, TypeVarKind, + context::InferContext, + diagnostic::{INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_TYPE_VARIABLE_DEFAULT}, + function::OverloadLiteral, + infer_definition_types, + typevar::TypeVarInstance, + visitor::find_over_type, + }, +}; +use itertools::Itertools; +use ruff_db::{ + diagnostic::{Annotation, Span}, + parsed::parsed_module, +}; +use ruff_python_ast as ast; +use ruff_text_size::{Ranged, TextRange}; + +pub(crate) fn check_function_definition<'db>( + context: &InferContext<'db, '_>, + definition: Definition<'db>, + file_expression_type: &impl Fn(&ast::Expr) -> Type<'db>, +) { + let db = context.db(); + + let Some(Type::FunctionLiteral(function_type)) = + infer_definition_types(db, definition).undecorated_type() + else { + return; + }; + + let last_definition = function_type.literal(db).last_definition(db); + let signature = last_definition.raw_signature(db); + + check_legacy_positional_only_convention(context, last_definition, &signature); + check_legacy_typevar_defaults(context, last_definition, &signature, file_expression_type); + check_legacy_typevar_ordering(context, last_definition, &signature, file_expression_type); +} + +/// Check for invalid applications of the pre-PEP-570 positional-only parameter convention. +fn check_legacy_positional_only_convention<'db>( + context: &InferContext<'db, '_>, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, +) { + let db = context.db(); + let node = last_definition.node(db, context.file(), context.module()); + let ast_parameters = &node.parameters; + + // If the function has any PEP-570 positional-only parameters, + // assume that `__`-prefixed parameters are not meant to be positional-only + if !ast_parameters.posonlyargs.is_empty() { + return; + } + let parsed_parameters = signature.parameters(); + let mut previous_non_positional_only: Option<&ast::ParameterWithDefault> = None; + + for (param_node, param) in std::iter::zip(ast_parameters, parsed_parameters) { + let ast::AnyParameterRef::NonVariadic(param_node) = param_node else { + continue; + }; + if param.is_positional_only() { + continue; + } + + // Valid uses of the PEP-484 positional-only convention will have been detected as such + // in the first iteration over this scope, so `param.is_positional_only()` will return `true` + // for those. We only get here for invalid uses of the PEP-484 positional-only convention. + if param_node.uses_pep_484_positional_only_convention() { + let Some(builder) = + context.report_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER, param_node.name()) + else { + continue; + }; + let mut diagnostic = builder.into_diagnostic( + "Invalid use of the legacy convention \ + for positional-only parameters", + ); + diagnostic.set_primary_message( + "Parameter name begins with `__` but will not be treated as positional-only", + ); + diagnostic.info( + "A parameter can only be positional-only \ + if it precedes all positional-or-keyword parameters", + ); + if let Some(earlier_node) = previous_non_positional_only { + diagnostic.annotate( + context + .secondary(earlier_node.name()) + .message("Prior parameter here was positional-or-keyword"), + ); + } + } else if previous_non_positional_only.is_none() { + previous_non_positional_only = Some(param_node); + } + } +} + +/// Check whether any legacy `TypeVar` used in a function signature has a default +/// that references an out-of-scope type variable. +/// +/// This check mirrors the class-level check at `report_invalid_typevar_default_reference`, +/// but for function/method generic contexts. +fn check_legacy_typevar_defaults<'db>( + context: &InferContext<'db, '_>, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, + file_expression_type: &impl Fn(&ast::Expr) -> Type<'db>, +) { + let db = context.db(); + + let Some(generic_context) = signature.generic_context else { + return; + }; + + let typevars = generic_context + .variables(db) + .map(|bound_tvar| bound_tvar.typevar(db)); + + for (i, typevar) in typevars.clone().enumerate() { + // Only check legacy TypeVars; PEP 695 type parameters are already validated + // by `check_default_for_outer_scope_typevars` in the type parameter scope. + if !matches!( + typevar.kind(db), + TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec + ) { + continue; + } + + let Some(default_ty) = typevar.default_type(db) else { + continue; + }; + + let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let tvar = match t { + Type::TypeVar(tvar) => tvar.typevar(db), + Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, + _ => return None, + }; + if !typevars.clone().take(i).contains(&tvar) { + Some(tvar) + } else { + None + } + }); + + let Some(bad_typevar) = first_bad_tvar else { + continue; + }; + + let is_later_in_list = typevars.clone().skip(i).contains(&bad_typevar); + let node = last_definition.node(db, context.file(), context.module()); + + let primary_range = + find_typevar_annotation_range(context, node, typevar, file_expression_type); + + let Some(builder) = context.report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, primary_range) + else { + continue; + }; + let typevar_name = typevar.name(db); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid use of type variable `{typevar_name}`", + )); + + if is_later_in_list { + diagnostic.set_primary_message(format_args!( + "Default of `{typevar_name}` references later type parameter `{}`", + bad_typevar.name(db), + )); + diagnostic.set_concise_message(format_args!( + "Invalid use of type variable `{typevar_name}`: default of `{typevar_name}` \ + refers to later parameter `{}`", + bad_typevar.name(db) + )); + } else { + diagnostic.set_primary_message(format_args!( + "Default of `{typevar_name}` references out-of-scope type variable `{}`", + bad_typevar.name(db), + )); + diagnostic.set_concise_message(format_args!( + "Invalid use of type variable `{typevar_name}`: default of `{typevar_name}` \ + refers to out-of-scope type variable `{}`", + bad_typevar.name(db) + )); + } + + if let Some(typevar_definition) = typevar.definition(db) { + let file = typevar_definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + typevar_definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{typevar_name}` defined here")), + ); + } + + diagnostic.info("See https://typing.python.org/en/latest/spec/generics.html#scoping-rules"); + } +} + +fn find_typevar_annotation_range<'db>( + context: &InferContext<'db, '_>, + node: &ast::StmtFunctionDef, + typevar: TypeVarInstance<'db>, + file_expression_type: impl Fn(&ast::Expr) -> Type<'db>, +) -> TextRange { + let db = context.db(); + let typevar_id = typevar.identity(db); + + node.parameters + .iter() + .filter_map(ast::AnyParameterRef::annotation) + .chain(node.returns.as_deref()) + .find(|ann| file_expression_type(ann).references_typevar(db, typevar_id)) + .map(Ranged::range) + .unwrap_or_else(|| node.name.range()) +} + +/// Check that legacy `TypeVar`s without defaults don't follow `TypeVar`s with defaults +/// in a function's generic context. +/// +/// This mirrors the class-level check using `report_invalid_type_param_order`, but for +/// function/method generic contexts using the `invalid-type-variable-default` lint. +fn check_legacy_typevar_ordering<'db>( + context: &InferContext<'db, '_>, + last_definition: OverloadLiteral<'db>, + signature: &Signature<'db>, + file_expression_type: &impl Fn(&ast::Expr) -> Type<'db>, +) { + struct State<'db> { + typevar_with_default: TypeVarInstance<'db>, + invalid_later_tvars: Vec>, + } + + let db = context.db(); + + let Some(generic_context) = signature.generic_context else { + return; + }; + + let mut state: Option> = None; + + for bound_typevar in generic_context.variables(db) { + let typevar = bound_typevar.typevar(db); + + // Only check legacy TypeVars; PEP 695 ordering is validated by the parser. + if !matches!( + typevar.kind(db), + TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec + ) { + continue; + } + + let has_default = typevar.default_type(db).is_some(); + + if let Some(state) = state.as_mut() { + if !has_default { + state.invalid_later_tvars.push(typevar); + } + } else if has_default { + state = Some(State { + typevar_with_default: typevar, + invalid_later_tvars: vec![], + }); + } + } + + let Some(state) = state else { + return; + }; + + if state.invalid_later_tvars.is_empty() { + return; + } + + let node = last_definition.node(db, context.file(), context.module()); + + let primary_range = find_typevar_annotation_range( + context, + node, + state.invalid_later_tvars[0], + file_expression_type, + ); + + let Some(builder) = context.report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, primary_range) else { + return; + }; + + let mut diagnostic = builder.into_diagnostic( + "Type parameters without defaults cannot follow type parameters with defaults", + ); + + let typevar_with_default_name = state.typevar_with_default.name(db); + + diagnostic.set_concise_message(format_args!( + "Type parameter `{}` without a default cannot follow \ + earlier parameter `{typevar_with_default_name}` with a default", + state.invalid_later_tvars[0].name(db), + )); + + if let [single_typevar] = &*state.invalid_later_tvars { + diagnostic.set_primary_message(format_args!( + "Type variable `{}` does not have a default", + single_typevar.name(db), + )); + } else { + let later_typevars = + format_enumeration(state.invalid_later_tvars.iter().map(|tv| tv.name(db))); + diagnostic.set_primary_message(format_args!( + "Type variables {later_typevars} do not have defaults", + )); + } + + let secondary_range = find_typevar_annotation_range( + context, + node, + state.typevar_with_default, + file_expression_type, + ); + + diagnostic.annotate(context.secondary(secondary_range).message(format_args!( + "Earlier TypeVar `{typevar_with_default_name}` has a default" + ))); + + for tvar in [state.typevar_with_default, state.invalid_later_tvars[0]] { + let Some(definition) = tvar.definition(db) else { + continue; + }; + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{}` defined here", tvar.name(db))), + ); + } +} diff --git a/crates/ty_python_semantic/src/types/infer/deferred/mod.rs b/crates/ty_python_semantic/src/types/infer/deferred/mod.rs new file mode 100644 index 0000000000000..f66ad455a2e25 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/deferred/mod.rs @@ -0,0 +1,9 @@ +//! A home for deferred checks that must be done after the `TypeInferenceBuilder` has done an initial +//! inference pass over the whole scope. + +pub(super) mod dynamic_class; +pub(super) mod final_variable; +pub(super) mod function; +pub(super) mod overloaded_function; +pub(super) mod static_class; +pub(super) mod typeguard; diff --git a/crates/ty_python_semantic/src/types/infer/deferred/overloaded_function.rs b/crates/ty_python_semantic/src/types/infer/deferred/overloaded_function.rs new file mode 100644 index 0000000000000..11c043c5a24b5 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/deferred/overloaded_function.rs @@ -0,0 +1,249 @@ +use ruff_db::diagnostic::Annotation; +use rustc_hash::FxHashSet; + +use crate::{ + place::{DefinedPlace, Definedness, Place, place_from_bindings}, + semantic_index::{ + SemanticIndex, definition::Definition, place::ScopedPlaceId, scope::NodeWithScopeKind, + }, + types::{ + KnownClass, Type, binding_type, + context::InferContext, + diagnostic::INVALID_OVERLOAD, + function::{FunctionDecorators, FunctionType, KnownFunction}, + }, +}; + +/// Check the overloaded functions in this scope. +/// +/// This only checks the overloaded functions that are: +/// 1. Visible publicly at the end of this scope +/// 2. Or, defined and called in this scope +/// +/// For (1), this has the consequence of not checking an overloaded function that is being +/// shadowed by another function with the same name in this scope. +pub(crate) fn check_overloaded_function<'db>( + context: &InferContext<'db, '_>, + ty: Type<'db>, + definition: Definition<'db>, + scope: &NodeWithScopeKind, + index: &SemanticIndex<'db>, + seen_overloaded_places: &mut FxHashSet, + seen_public_functions: &mut FxHashSet>, +) { + // Collect all the unique overloaded function places in this scope. This requires a set + // because an overloaded function uses the same place for each of the overloads and the + // implementation. + let Type::FunctionLiteral(function) = ty else { + return; + }; + + let db = context.db(); + + if function.file(db) != context.file() { + // If the function is not in this file, we don't need to check it. + // https://github.com/astral-sh/ruff/pull/17609#issuecomment-2839445740 + return; + } + + if !function.has_known_decorator(db, FunctionDecorators::OVERLOAD) { + return; + } + + let place = definition.place(db); + + if !seen_overloaded_places.insert(place) { + // We have already checked this overloaded function in this scope, so we can skip it. + return; + } + + let use_def = index.use_def_map(context.scope().file_scope_id(db)); + + let Place::Defined(DefinedPlace { + ty: Type::FunctionLiteral(function), + definedness: Definedness::AlwaysDefined, + .. + }) = place_from_bindings( + db, + use_def.end_of_scope_symbol_bindings(place.as_symbol().unwrap()), + ) + .place + else { + return; + }; + + if !seen_public_functions.insert(function) { + // We have already checked this overloaded function as a public function, so we can skip it. + return; + } + + let (overloads, implementation) = function.overloads_and_implementation(db); + if overloads.is_empty() { + return; + } + + // Check that the overloaded function has at least two overloads + if let [single_overload] = overloads { + let function_node = single_overload.node(db, context.file(), context.module()); + if let Some(builder) = context.report_lint(&INVALID_OVERLOAD, &function_node.name) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Overloaded function `{}` requires at least two overloads", + &function_node.name + )); + diagnostic.set_primary_message("Only one overload defined here"); + } + } + + // Check that the overloaded function has an implementation. Overload definitions + // within stub files, protocols, and on abstract methods within abstract base classes + // are exempt from this check. + if implementation.is_none() && !context.in_stub() { + let mut implementation_required = true; + + if function + .iter_overloads_and_implementation(db) + .all(|f| f.body_scope(db).scope(db).in_type_checking_block()) + { + implementation_required = false; + } else if let NodeWithScopeKind::Class(class_node_ref) = scope { + let class = binding_type( + db, + index.expect_single_definition(class_node_ref.node(context.module())), + ) + .expect_class_literal(); + + if class.is_protocol(db) + || (Type::ClassLiteral(class) + .is_subtype_of(db, KnownClass::ABCMeta.to_instance(db)) + && overloads.iter().all(|overload| { + overload.has_known_decorator(db, FunctionDecorators::ABSTRACT_METHOD) + })) + { + implementation_required = false; + } + } + + if implementation_required { + let function_node = overloads[0].node(db, context.file(), context.module()); + if let Some(builder) = context.report_lint(&INVALID_OVERLOAD, &function_node.name) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Overloads for function `{}` must be followed by a non-`@overload`-decorated implementation function", + &function_node.name + )); + diagnostic.info(format_args!( + "Attempting to call `{}` will raise `TypeError` at runtime", + &function_node.name + )); + diagnostic.info("Overloaded functions without implementations are only permitted:"); + diagnostic.info(" - in stub files"); + diagnostic.info(" - in `if TYPE_CHECKING` blocks"); + diagnostic.info(" - as methods on protocol classes"); + diagnostic.info(" - or as `@abstractmethod`-decorated methods on abstract classes"); + diagnostic.info( + "See https://docs.python.org/3/library/typing.html#typing.overload \ + for more details", + ); + } + } + } + + for (decorator, name) in [ + (FunctionDecorators::CLASSMETHOD, "classmethod"), + (FunctionDecorators::STATICMETHOD, "staticmethod"), + ] { + let mut decorator_present = false; + let mut decorator_missing = vec![]; + + for function in overloads.iter().chain(implementation.as_ref()) { + if function.has_known_decorator(db, decorator) { + decorator_present = true; + } else { + decorator_missing.push(function); + } + } + + if !decorator_present { + // Both overloads and implementation does not have the decorator + continue; + } + if decorator_missing.is_empty() { + // All overloads and implementation have the decorator + continue; + } + + let function_node = function.node(db, context.file(), context.module()); + if let Some(builder) = context.report_lint(&INVALID_OVERLOAD, &function_node.name) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Overloaded function `{}` does not use the `@{name}` decorator \ + consistently", + &function_node.name + )); + for function in decorator_missing { + diagnostic.annotate( + context + .secondary(function.focus_range(db, context.module())) + .message(format_args!("Missing here")), + ); + } + } + } + + for (function, decorator) in [ + (KnownFunction::Final, FunctionDecorators::FINAL), + (KnownFunction::Override, FunctionDecorators::OVERRIDE), + ] { + if let Some(implementation) = implementation { + for overload in overloads { + if !overload.has_known_decorator(db, decorator) { + continue; + } + let function_node = overload.node(db, context.file(), context.module()); + let Some(builder) = context.report_lint(&INVALID_OVERLOAD, &function_node.name) + else { + continue; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@{name}` decorator should be applied only to the \ + overload implementation", + name = function.name() + )); + if let Some(decorator) = overload.find_known_decorator_span(db, function) { + diagnostic.annotate(Annotation::secondary(decorator)); + } + diagnostic.annotate( + context + .secondary(implementation.focus_range(db, context.module())) + .message(format_args!("Implementation defined here")), + ); + } + } else { + let mut overloads = overloads.iter(); + let Some(first_overload) = overloads.next() else { + continue; + }; + for overload in overloads { + if !overload.has_known_decorator(db, decorator) { + continue; + } + let function_node = overload.node(db, context.file(), context.module()); + let Some(builder) = context.report_lint(&INVALID_OVERLOAD, &function_node.name) + else { + continue; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@{name}` decorator should be applied only to the \ + first overload", + name = function.name() + )); + if let Some(decorator) = overload.find_known_decorator_span(db, function) { + diagnostic.annotate(Annotation::secondary(decorator)); + } + diagnostic.annotate( + context + .secondary(first_overload.focus_range(db, context.module())) + .message(format_args!("First overload defined here")), + ); + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs b/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs new file mode 100644 index 0000000000000..16d9a79917508 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs @@ -0,0 +1,1260 @@ +use itertools::Itertools; +use ruff_db::{ + diagnostic::{Annotation, Span, SubDiagnostic, SubDiagnosticSeverity}, + parsed::parsed_module, + source::source_text, +}; +use ruff_diagnostics::{Edit, Fix}; +use ruff_python_ast as ast; +use ruff_text_size::{Ranged, TextRange, TextSize}; +use rustc_hash::FxHashMap; + +use crate::{ + TypeQualifiers, + diagnostic::format_enumeration, + place::{place_from_bindings, place_from_declarations}, + semantic_index::{ + SemanticIndex, attribute_assignments, definition::DefinitionKind, scope::ScopeId, + }, + types::{ + CallArguments, ClassBase, ClassLiteral, ClassType, GenericAlias, KnownInstanceType, + MemberLookupPolicy, MetaclassCandidate, Parameters, Signature, SpecialFormType, + StaticClassLiteral, Type, + call::{Argument, CallError, CallErrorKind}, + class::{AbstractMethod, CodeGeneratorKind, FieldKind, MetaclassErrorKind}, + context::InferContext, + definition_expression_type, + diagnostic::{ + ABSTRACT_METHOD_IN_FINAL_CLASS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, + DATACLASS_FIELD_ORDER, DUPLICATE_KW_ONLY, FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, + INVALID_ARGUMENT_TYPE, INVALID_BASE, INVALID_DATACLASS, INVALID_GENERIC_CLASS, + INVALID_GENERIC_ENUM, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_PROTOCOL, + INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, + SUBCLASS_OF_FINAL_CLASS, UNKNOWN_ARGUMENT, report_bad_frozen_dataclass_inheritance, + report_conflicting_metaclass_from_bases, report_duplicate_bases, + report_instance_layout_conflict, report_invalid_or_unsupported_base, + report_invalid_total_ordering, report_invalid_type_param_order, + report_invalid_typevar_default_reference, + report_named_tuple_field_with_leading_underscore, + report_namedtuple_field_without_default_after_field_with_default, + report_shadowed_type_variable, report_unsupported_base, + }, + enums::is_enum_class_by_inheritance, + function::KnownFunction, + generics::enclosing_generic_contexts, + infer_definition_types, + mro::StaticMroErrorKind, + overrides, + tuple::Tuple, + typevar::TypeVarInstance, + visitor::find_over_type, + }, +}; + +/// Iterate over all static class definitions (created using `class` statements) to check that +/// the definition will not cause an exception to be raised at runtime. This needs to be done +/// after most other types in the scope have been inferred, due to the fact that base classes +/// can be deferred. If it looks like a class definition is invalid in some way, issue a +/// diagnostic. +/// +/// Note: Dynamic classes created via `type()` calls are checked separately during type +/// inference of the call expression. +/// +/// Among the things we check for in this method are whether Python will be able to determine a +/// consistent "[method resolution order]" and [metaclass] for each class. +/// +/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order +/// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses +pub(crate) fn check_static_class_definitions<'db>( + context: &InferContext<'db, '_>, + ty: Type<'db>, + class_node: &ast::StmtClassDef, + index: &SemanticIndex<'db>, + file_expression_type: &impl Fn(&ast::Expr) -> Type<'db>, +) { + let db = context.db(); + + let Type::ClassLiteral(ClassLiteral::Static(class)) = ty else { + return; + }; + + // Check that the class does not have a cyclic definition + if let Some(inheritance_cycle) = class.inheritance_cycle(db) { + if inheritance_cycle.is_participant() + && let Some(builder) = context.report_lint(&CYCLIC_CLASS_DEFINITION, class_node) + { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}` (class cannot inherit from itself)", + class.name(db) + )); + } + + // If a class is cyclically defined, that's a sufficient error to report; the + // following checks (which are all inheritance-based) aren't even relevant. + return; + } + + // Check that the class is not an enum and generic + if is_enum_class_by_inheritance(db, class) && class.generic_context(db).is_some() { + if let Some(builder) = context.report_lint(&INVALID_GENERIC_ENUM, class_node) { + builder.into_diagnostic(format_args!( + "Enum class `{}` cannot be generic", + class.name(db) + )); + } + } + + let class_kind = CodeGeneratorKind::from_class(db, class.into(), None); + + // If it's a `NamedTuple` class, check that no field without a default value + // appears after a field with a default value. + if class_kind == Some(CodeGeneratorKind::NamedTuple) { + let mut field_with_default_encountered = None; + + for (field_name, field) in class.own_fields(db, None, CodeGeneratorKind::NamedTuple) { + if field_name.starts_with('_') { + report_named_tuple_field_with_leading_underscore( + context, + class, + &field_name, + field.first_declaration, + ); + } + if matches!( + field.kind, + FieldKind::NamedTuple { + default_ty: Some(_) + } + ) { + field_with_default_encountered = Some((field_name, field.first_declaration)); + } else if let Some(field_with_default) = field_with_default_encountered.as_ref() { + report_namedtuple_field_without_default_after_field_with_default( + context, + class, + (&field_name, field.first_declaration), + field_with_default, + ); + } + } + } + + let is_protocol = class.is_protocol(db); + + // Check for invalid `@dataclass` applications. + if class.dataclass_params(db).is_some() { + if class.has_named_tuple_class_in_mro(db) { + if let Some(builder) = context.report_lint(&INVALID_DATACLASS, class.header_range(db)) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "`NamedTuple` class `{}` cannot be decorated with `@dataclass`", + class.name(db), + )); + diagnostic + .info("An exception will be raised when instantiating the class at runtime"); + } + } else if class.is_typed_dict(db) { + if let Some(builder) = context.report_lint(&INVALID_DATACLASS, class.header_range(db)) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "`TypedDict` class `{}` cannot be decorated with `@dataclass`", + class.name(db), + )); + diagnostic.info( + "An exception will often be raised when instantiating the class at runtime", + ); + } + } else if is_enum_class_by_inheritance(db, class) { + if let Some(builder) = context.report_lint(&INVALID_DATACLASS, class.header_range(db)) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Enum class `{}` cannot be decorated with `@dataclass`", + class.name(db), + )); + diagnostic.info("Applying `@dataclass` to an enum is not supported at runtime"); + } + } else if is_protocol { + if let Some(builder) = context.report_lint(&INVALID_DATACLASS, class.header_range(db)) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Protocol class `{}` cannot be decorated with `@dataclass`", + class.name(db), + )); + diagnostic.info("Protocols define abstract interfaces and cannot be instantiated"); + } + } + } + + let mut disjoint_bases = IncompatibleBases::default(); + let mut protocol_base_with_generic_context = None; + + // Iterate through the class's explicit bases to check for various possible errors: + // - Check for inheritance from plain `Generic`, + // - Check for inheritance from a `@final` classes + // - If the class is a protocol class: check for inheritance from a non-protocol class + // - If the class is a NamedTuple class: check for multiple inheritance that isn't `Generic[]` + for (i, base_class) in class.explicit_bases(db).iter().enumerate() { + if class_kind == Some(CodeGeneratorKind::NamedTuple) + && !matches!( + base_class, + Type::SpecialForm(SpecialFormType::NamedTuple) + | Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)) + ) + { + if let Some(builder) = context.report_lint(&INVALID_NAMED_TUPLE, &class_node.bases()[i]) + { + builder.into_diagnostic(format_args!( + "NamedTuple class `{}` cannot use multiple inheritance except with `Generic[]`", + class.name(db), + )); + } + } + + let base_class = match base_class { + Type::SpecialForm(SpecialFormType::Generic) => { + if let Some(builder) = context.report_lint(&INVALID_BASE, &class_node.bases()[i]) { + // Unsubscripted `Generic` can appear in the MRO of many classes, + // but it is never valid as an explicit base class in user code. + builder.into_diagnostic("Cannot inherit from plain `Generic`"); + } + continue; + } + Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(new_context)) => { + let Some((previous_index, previous_context)) = protocol_base_with_generic_context + else { + continue; + }; + let prior_node = &class_node.bases()[previous_index]; + let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, prior_node) else { + continue; + }; + let mut diagnostic = builder.into_diagnostic( + "Cannot both inherit from subscripted `Protocol` \ + and subscripted `Generic`", + ); + if let ast::Expr::Subscript(prior_node) = prior_node + && new_context == previous_context + { + diagnostic.help("Remove the type parameters from the `Protocol` base"); + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new( + prior_node.value.end(), + prior_node.end(), + )))); + } + continue; + } + // Note that unlike several of the other errors caught in this function, + // this does not lead to the class creation failing at runtime, + // but it is semantically invalid. + Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(generic_context)) => { + if let Some(type_params) = class_node.type_params.as_deref() { + let Some(builder) = + context.report_lint(&INVALID_GENERIC_CLASS, &class_node.bases()[i]) + else { + continue; + }; + let mut diagnostic = builder.into_diagnostic( + "Cannot both inherit from subscripted `Protocol` \ + and use PEP 695 type variables", + ); + if let ast::Expr::Subscript(node) = &class_node.bases()[i] { + let source = source_text(db, context.file()); + let type_params_range = TextRange::new( + type_params.start().saturating_add(TextSize::new(1)), + type_params.end().saturating_sub(TextSize::new(1)), + ); + if source[node.slice.range()] == source[type_params_range] { + diagnostic.help("Remove the type parameters from the `Protocol` base"); + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion( + TextRange::new(node.value.end(), node.end()), + ))); + } + } + } else if protocol_base_with_generic_context.is_none() { + protocol_base_with_generic_context = Some((i, generic_context)); + } + continue; + } + Type::ClassLiteral(class) => ClassType::NonGeneric(*class), + Type::GenericAlias(class) => ClassType::Generic(*class), + _ => continue, + }; + + if let Some(disjoint_base) = base_class.nearest_disjoint_base(db) { + disjoint_bases.insert(disjoint_base, i, base_class.class_literal(db)); + } + + if is_protocol { + if !base_class.is_protocol(db) + && !base_class.is_object(db) + && let Some(builder) = + context.report_lint(&INVALID_PROTOCOL, &class_node.bases()[i]) + { + builder.into_diagnostic(format_args!( + "Protocol class `{}` cannot inherit from non-protocol class `{}`", + class.name(db), + base_class.name(db), + )); + } + } else if class_kind == Some(CodeGeneratorKind::TypedDict) { + if !base_class.class_literal(db).is_typed_dict(db) + && let Some(builder) = + context.report_lint(&INVALID_TYPED_DICT_HEADER, &class_node.bases()[i]) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "TypedDict class `{}` can only inherit from TypedDict classes", + class.name(db), + )); + diagnostic.set_primary_message(format_args!( + "`{}` is not a `TypedDict` class", + base_class.name(db) + )); + diagnostic.annotate( + Annotation::secondary(base_class.class_literal(db).header_span(db)) + .message(format_args!("`{}` defined here", base_class.name(db))), + ); + } + } + + if base_class.is_final(db) { + if let Some(builder) = + context.report_lint(&SUBCLASS_OF_FINAL_CLASS, &class_node.bases()[i]) + { + builder.into_diagnostic(format_args!( + "Class `{}` cannot inherit from final class `{}`", + class.name(db), + base_class.name(db), + )); + } + } + + if let Some((base_class_literal, _)) = base_class.static_class_literal(db) + && let (Some(base_is_frozen), Some(class_is_frozen)) = ( + base_class_literal.is_frozen_dataclass(db), + class.is_frozen_dataclass(db), + ) + && base_is_frozen != class_is_frozen + { + report_bad_frozen_dataclass_inheritance( + context, + class, + class_node, + base_class_literal, + &class_node.bases()[i], + base_is_frozen, + ); + } + } + + // Check for starred variable-length tuples that cannot be unpacked + let class_definition = index.expect_single_definition(class_node); + for base in class_node.bases() { + if let ast::Expr::Starred(starred) = base + && let starred_ty = definition_expression_type(db, class_definition, &starred.value) + && let Some(tuple_spec) = starred_ty.tuple_instance_spec(db) + && !matches!(tuple_spec.as_ref(), Tuple::Fixed(_)) + { + report_unsupported_base(context, base, starred_ty, class); + } + } + + // Check that the class's MRO is resolvable + match class.try_mro(db, None) { + Err(mro_error) => match mro_error.reason() { + StaticMroErrorKind::DuplicateBases(duplicates) => { + let base_nodes = class_node.bases(); + for duplicate in duplicates { + report_duplicate_bases(context, class, duplicate, base_nodes); + } + } + StaticMroErrorKind::InvalidBases(bases) => { + let base_nodes = class_node.bases(); + for (index, base_ty) in bases { + report_invalid_or_unsupported_base( + context, + &base_nodes[*index], + *base_ty, + class, + ); + } + } + StaticMroErrorKind::UnresolvableMro { + bases_list, + generic_index, + } => { + if let Some(builder) = + context.report_lint(&INCONSISTENT_MRO, class.header_range(db)) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot create a consistent method resolution order (MRO) \ + for class `{}` with bases list `[{}]`", + class.name(db), + bases_list.iter().map(|base| base.display(db)).join(", ") + )); + if let Some(index) = *generic_index + && let [first_base, .., last_base] = class_node.bases() + { + let source = source_text(db, context.file()); + let generic_base = &source[class_node.bases()[index].range()]; + diagnostic.help(format_args!( + "Move `{generic_base}` to the end of the bases list" + )); + let reordered_bases = class_node + .bases() + .iter() + .enumerate() + .filter(|(i, _)| *i != index) + .map(|(_, base)| &source[base.range()]) + .chain(std::iter::once(generic_base)) + .join(", "); + let fix = Fix::unsafe_edit(Edit::range_replacement( + reordered_bases, + TextRange::new(first_base.start(), last_base.end()), + )); + diagnostic.set_fix(fix); + } + } + } + StaticMroErrorKind::Pep695ClassWithGenericInheritance => { + if let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, class_node) { + builder.into_diagnostic( + "Cannot both inherit from `typing.Generic` \ + and use PEP 695 type variables", + ); + } + } + StaticMroErrorKind::InheritanceCycle => { + if let Some(builder) = context.report_lint(&CYCLIC_CLASS_DEFINITION, class_node) { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}` (class cannot inherit from itself)", + class.name(db) + )); + } + } + }, + Ok(_) => { + disjoint_bases.remove_redundant_entries(db); + + if disjoint_bases.len() > 1 { + report_instance_layout_conflict( + context, + class.header_range(db), + Some(class_node.bases()), + &disjoint_bases, + ); + } + + // Check for inconsistent specializations of the same generic + // base class. This detects when different explicit bases + // contribute conflicting specializations of a common generic + // ancestor to the MRO. For example: + // + // class Grandparent(Generic[T1, T2]): ... + // class Parent(Grandparent[T1, T2]): ... + // class BadChild(Parent[T1, T2], Grandparent[T2, T1]): ... # Error + let explicit_bases = class.explicit_bases(db); + let can_annotate_bases = || { + class_node.bases().len() == explicit_bases.len() + && !class_node.bases().iter().any(ast::Expr::is_starred_expr) + }; + + // Maps each generic ancestor's class literal to the first + // specialization seen and the index of the explicit base it + // came from. + let mut ancestor_specs = + FxHashMap::, (GenericAlias<'db>, usize)>::default(); + + 'outer: for (i, base) in explicit_bases.iter().enumerate() { + let base_class = match base { + Type::GenericAlias(c) => ClassType::Generic(*c), + Type::ClassLiteral(c) if c.generic_context(db).is_none() => { + ClassType::NonGeneric(*c) + } + _ => continue, + }; + + for supercls in base_class.iter_mro(db) { + let ClassBase::Class(ClassType::Generic(supercls_alias)) = supercls else { + continue; + }; + let origin = supercls_alias.origin(db); + + if let Some(&(earlier_alias, earlier_idx)) = ancestor_specs.get(&origin) { + if earlier_idx != i + && earlier_alias + .specialization(db) + .types(db) + .iter() + .zip(supercls_alias.specialization(db).types(db)) + .any(|(t1, t2)| !t1.is_dynamic() && !t2.is_dynamic() && t1 != t2) + { + let Some(builder) = + context.report_lint(&INVALID_GENERIC_CLASS, class.header_range(db)) + else { + break 'outer; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Inconsistent type arguments for `{}` among class bases", + origin.name(db) + )); + + let later_is_direct = matches!( + base, + Type::GenericAlias(a) + if a.origin(db) == origin + ); + + if can_annotate_bases() { + diagnostic.annotate( + context.secondary(&class_node.bases()[earlier_idx]).message( + format_args!( + "Earlier class base inherits from `{}`", + earlier_alias.display(db) + ), + ), + ); + let later_annotation = context.secondary(&class_node.bases()[i]); + diagnostic.annotate(if later_is_direct { + later_annotation.message(format_args!( + "Later class base is `{}`", + supercls_alias.display(db) + )) + } else { + later_annotation.message(format_args!( + "Later class base inherits from `{}`", + supercls_alias.display(db) + )) + }); + } else { + diagnostic.info(format_args!( + "Earlier class base inherits from `{}`", + earlier_alias.display(db) + )); + if later_is_direct { + diagnostic.info(format_args!( + "Later class base is `{}`", + supercls_alias.display(db) + )); + } else { + diagnostic.info(format_args!( + "Later class base inherits from `{}`", + supercls_alias.display(db) + )); + } + } + diagnostic.set_concise_message(format_args!( + "Inconsistent type arguments: class cannot \ + inherit from both `{}` and `{}`", + supercls_alias.display(db), + earlier_alias.display(db) + )); + break 'outer; + } + } else if !supercls_alias + .specialization(db) + .types(db) + .iter() + .all(Type::is_dynamic) + { + ancestor_specs.insert(origin, (supercls_alias, i)); + } + } + } + } + } + + // Check that `@total_ordering` has a valid ordering method in the MRO + if class.total_ordering(db) && !class.has_ordering_method_in_mro(db, None) { + // Find the `@total_ordering` decorator to report the diagnostic at its location + if let Some(decorator) = class_node.decorator_list.iter().find(|decorator| { + file_expression_type(&decorator.expression) + .as_function_literal() + .is_some_and(|function| function.is_known(db, KnownFunction::TotalOrdering)) + }) { + report_invalid_total_ordering(context, ClassLiteral::Static(class), decorator); + } + } + + // Check that the class's metaclass can be determined without error. + if let Err(metaclass_error) = class.try_metaclass(db) { + match metaclass_error.reason() { + MetaclassErrorKind::Cycle => { + if let Some(builder) = context.report_lint(&CYCLIC_CLASS_DEFINITION, class_node) { + builder + .into_diagnostic(format_args!("Cyclic definition of `{}`", class.name(db))); + } + } + MetaclassErrorKind::GenericMetaclass => { + if let Some(builder) = context.report_lint(&INVALID_METACLASS, class_node) { + builder.into_diagnostic("Generic metaclasses are not supported"); + } + } + MetaclassErrorKind::NotCallable(ty) => { + if let Some(builder) = context.report_lint(&INVALID_METACLASS, class_node) { + builder.into_diagnostic(format_args!( + "Metaclass type `{}` is not callable", + ty.display(db) + )); + } + } + MetaclassErrorKind::PartlyNotCallable(ty) => { + if let Some(builder) = context.report_lint(&INVALID_METACLASS, class_node) { + builder.into_diagnostic(format_args!( + "Metaclass type `{}` is partly not callable", + ty.display(db) + )); + } + } + MetaclassErrorKind::Conflict { + candidate1: + MetaclassCandidate { + metaclass: metaclass1, + explicit_metaclass_of: class1, + }, + candidate2: + MetaclassCandidate { + metaclass: metaclass2, + explicit_metaclass_of: class2, + }, + candidate1_is_base_class, + } => { + if *candidate1_is_base_class { + report_conflicting_metaclass_from_bases( + context, + class_node.into(), + class.name(db), + *metaclass1, + class1.name(db), + *metaclass2, + class2.name(db), + ); + } else if let Some(builder) = + context.report_lint(&CONFLICTING_METACLASS, class_node) + { + builder.into_diagnostic(format_args!( + "The metaclass of a derived class (`{class}`) \ + must be a subclass of the metaclasses of all its bases, \ + but `{metaclass_of_class}` (metaclass of `{class}`) \ + and `{metaclass_of_base}` (metaclass of base class `{base}`) \ + have no subclass relationship", + class = class.name(db), + metaclass_of_class = metaclass1.name(db), + metaclass_of_base = metaclass2.name(db), + base = class2.name(db), + )); + } + } + } + } + + // Check that the class arguments matches the arguments of the + // base class `__init_subclass__` method. + if let Some(args) = class_node.arguments.as_deref() { + if class_kind == Some(CodeGeneratorKind::TypedDict) { + for keyword in &args.keywords { + match keyword.arg.as_deref() { + Some(arg_name @ ("total" | "closed")) => { + let passed_type = file_expression_type(&keyword.value); + if passed_type + .as_literal_value() + .is_none_or(|literal| !literal.is_bool()) + && let Some(builder) = + context.report_lint(&INVALID_ARGUMENT_TYPE, keyword) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter `{arg_name}` \ + in `TypedDict` definition", + )); + diagnostic.set_primary_message(format_args!( + "Expected either `True` or `False`, got object of type `{}`", + passed_type.display(db) + )); + } + } + Some("extra_items") => { + // TODO: validate that passed arguments here are annotation expressions + } + Some("metaclass") => { + if let Some(builder) = + context.report_lint(&INVALID_TYPED_DICT_HEADER, keyword) + { + builder.into_diagnostic(format_args!( + "Custom metaclasses are not supported in `TypedDict` definitions", + )); + } + } + Some(other) => { + if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, keyword) { + builder.into_diagnostic(format_args!( + "Unknown keyword argument `{other}` \ + in `TypedDict` definition", + )); + } + } + None => { + if let Some(builder) = + context.report_lint(&INVALID_TYPED_DICT_HEADER, keyword) + { + builder.into_diagnostic(format_args!( + "Keyword-variadic arguments are not supported \ + in `TypedDict` definitions", + )); + } + } + } + } + } else { + let call_args: CallArguments = args + .keywords + .iter() + .filter_map(|keyword| match keyword.arg.as_ref() { + // We mimic the runtime behaviour and discard the metaclass argument + Some(name) if name.id.as_str() == "metaclass" => None, + Some(name) => { + let ty = file_expression_type(&keyword.value); + Some((Argument::Keyword(name.id.as_str()), Some(ty))) + } + None => { + let ty = file_expression_type(&keyword.value); + Some((Argument::Keywords, Some(ty))) + } + }) + .collect(); + + let init_subclass_type = class + .class_member_from_mro( + db, + "__init_subclass__", + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + // skip(1) to skip the current class and only consider base classes. + class.iter_mro(db, None).skip(1), + ) + .ignore_possibly_undefined(); + + if let Some(init_subclass) = init_subclass_type { + let call_args = call_args.with_self(Some(Type::from(class))); + if let Err(CallError(CallErrorKind::BindingError, bindings)) = + init_subclass.try_call(db, &call_args) + { + bindings.report_diagnostics(context, class_node.into()); + } + } + } + } + + // If the class is generic, verify that its generic context does not violate any of + // the typevar scoping rules. + if let (Some(legacy), Some(inherited)) = ( + class.legacy_generic_context(db), + class.inherited_legacy_generic_context(db), + ) { + if !inherited.is_subset_of(db, legacy) + && let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, class_node) + { + builder.into_diagnostic( + "`Generic` base class must include all type \ + variables used in other base classes", + ); + } + } + + if context.is_lint_enabled(&INVALID_GENERIC_CLASS) { + if !class.has_pep_695_type_params(db) + && let Some(generic_context) = class.legacy_generic_context(db) + { + struct State<'db> { + typevar_with_default: TypeVarInstance<'db>, + invalid_later_tvars: Vec>, + } + + let mut state: Option> = None; + + for bound_typevar in generic_context.variables(db) { + let typevar = bound_typevar.typevar(db); + let has_default = typevar.default_type(db).is_some(); + + if let Some(state) = state.as_mut() { + if !has_default { + state.invalid_later_tvars.push(typevar); + } + } else if has_default { + state = Some(State { + typevar_with_default: typevar, + invalid_later_tvars: vec![], + }); + } + } + + if let Some(state) = state + && !state.invalid_later_tvars.is_empty() + { + report_invalid_type_param_order( + context, + class, + class_node, + state.typevar_with_default, + &state.invalid_later_tvars, + ); + } + } + + // Check that type variable defaults only reference type variables + // that precede them in the type parameter list. + if let Some(generic_context) = class + .pep695_generic_context(db) + .or(class.legacy_generic_context(db)) + { + let typevars = generic_context.variables(db).map(|btv| btv.typevar(db)); + + // `variables` should be fairly cheap to clone; it's just several cheap wrappers around + // a `std::slice::Iter` under the hood. + for (i, typevar) in typevars.clone().enumerate() { + let Some(default_ty) = typevar.default_type(db) else { + continue; + }; + + let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let tvar = match t { + Type::TypeVar(tvar) => tvar.typevar(db), + Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, + _ => return None, + }; + if !typevars.clone().take(i).contains(&tvar) { + Some(tvar) + } else { + None + } + }); + if let Some(bad_typevar) = first_bad_tvar { + let is_later_in_list = typevars.clone().skip(i).contains(&bad_typevar); + report_invalid_typevar_default_reference( + context, + class, + typevar, + bad_typevar, + is_later_in_list, + ); + } + } + } + + let scope = class.body_scope(db).scope(db); + if let Some(parent) = scope.parent() { + // Check that the class's own type parameters don't shadow + // type variables from enclosing scopes (by name). + if let Some(generic_context) = class.generic_context(db) { + for self_typevar in generic_context.variables(db) { + let name = self_typevar.typevar(db).name(db); + for enclosing in enclosing_generic_contexts(db, index, parent) { + if let Some(other_typevar) = enclosing.binds_named_typevar(db, name) { + report_shadowed_type_variable( + context, + name, + "class", + &class_node.name.id, + class.header_range(db), + other_typevar, + ); + } + } + } + } + + // Check that the class's base classes don't reference type + // variables from enclosing scopes (by identity). + for base_typevar in class.typevars_referenced_in_bases(db) { + let typevar = base_typevar.typevar(db); + for enclosing in enclosing_generic_contexts(db, index, parent) { + if let Some(other_typevar) = enclosing.binds_typevar(db, typevar) { + report_shadowed_type_variable( + context, + typevar.name(db), + "class", + &class_node.name.id, + class.header_range(db), + other_typevar, + ); + } + } + } + } + } + + // Check that a dataclass does not have more than one `KW_ONLY` + // and that required fields are defined before default fields. + if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) = + CodeGeneratorKind::from_class(db, class.into(), None) + { + let specialization = None; + + let mut kw_only_sentinel_fields = vec![]; + let mut required_after_default_field_names = vec![]; + let mut has_seen_default_field = false; + + for (name, field) in class.own_fields(db, specialization, field_policy) { + if field.is_kw_only_sentinel(db) { + kw_only_sentinel_fields.push(name); + continue; + } + + // Extract dataclass field properties + let FieldKind::Dataclass { + default_ty, + init, + kw_only, + .. + } = &field.kind + else { + continue; + }; + + // Fields with init=False or kw_only=true don't participate in ordering check + if !init || *kw_only == Some(true) { + continue; + } + + if default_ty.is_some() { + has_seen_default_field = true; + } else if has_seen_default_field { + required_after_default_field_names.push(name); + } + } + + if kw_only_sentinel_fields.len() > 1 { + // TODO: The fields should be displayed in a subdiagnostic. + if let Some(builder) = context.report_lint(&DUPLICATE_KW_ONLY, &class_node.name) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Dataclass has more than one field annotated with `KW_ONLY`" + )); + + diagnostic.info(format_args!( + "`KW_ONLY` fields: {}", + kw_only_sentinel_fields + .iter() + .map(|name| format!("`{name}`")) + .join(", ") + )); + } + } + + if !required_after_default_field_names.is_empty() { + // Report field ordering violations + let body_scope = class.body_scope(db).file_scope_id(db); + let use_def_map = index.use_def_map(body_scope); + let place_table = index.place_table(body_scope); + + for name in required_after_default_field_names { + let Some(symbol_id) = place_table.symbol_id(name.as_str()) else { + continue; + }; + for decl_with_constraints in use_def_map.end_of_scope_symbol_declarations(symbol_id) + { + let Some(definition) = decl_with_constraints.declaration.definition() else { + continue; + }; + let DefinitionKind::AnnotatedAssignment(ann_assign) = definition.kind(db) + else { + continue; + }; + let Some(builder) = context + .report_lint(&DATACLASS_FIELD_ORDER, ann_assign.target(context.module())) + else { + continue; + }; + builder.into_diagnostic(format_args!( + "Required field `{name}` cannot be defined \ + after fields with default values", + )); + + break; + } + } + } + } + + // (13) Check for violations of the Liskov Substitution Principle, + // and for violations of other rules relating to invalid overrides of some sort. + overrides::check_class(context, class); + + // (14) Check for unimplemented abstract methods on final classes. + check_final_class_abstract_methods(context, class, class_node); + + // (15) Check for Final-qualified declarations without a value. + check_class_final_without_value(context, class, index); + + if let Some(protocol) = class.into_protocol_class(db) { + protocol.validate_members(context); + } + + // (16) If it's a `TypedDict` class, check that it doesn't include any invalid + // statements: https://typing.python.org/en/latest/spec/typeddict.html#class-based-syntax + // + // The body of the class definition defines the items of the `TypedDict` type. It + // may also contain a docstring or pass statements (primarily to allow the creation + // of an empty `TypedDict`). No other statements are allowed, and type checkers + // should report an error if any are present. + if class.is_typed_dict(db) { + for stmt in &class_node.body { + match stmt { + // Annotated assignments are allowed (that's the whole point), but they're + // not allowed to have a value. + ast::Stmt::AnnAssign(ann_assign) => { + if let Some(value) = &ann_assign.value + && let Some(builder) = + context.report_lint(&INVALID_TYPED_DICT_STATEMENT, &**value) + { + builder.into_diagnostic("TypedDict item cannot have a value"); + } + + continue; + } + // Pass statements are allowed. + ast::Stmt::Pass(_) => continue, + ast::Stmt::Expr(expr) => { + // Docstrings are allowed. + if matches!(*expr.value, ast::Expr::StringLiteral(_)) { + continue; + } + // As a non-standard but common extension, we also interpret `...` as + // equivalent to `pass`. + if matches!(*expr.value, ast::Expr::EllipsisLiteral(_)) { + continue; + } + } + // Everything else is forbidden. + _ => {} + } + if let Some(builder) = context.report_lint(&INVALID_TYPED_DICT_STATEMENT, stmt) { + if matches!(stmt, ast::Stmt::FunctionDef(_)) { + builder.into_diagnostic(format_args!("TypedDict class cannot have methods")); + } else { + let mut diagnostic = builder + .into_diagnostic(format_args!("invalid statement in TypedDict class body")); + diagnostic.info("Only annotated declarations (`: `) are allowed."); + } + } + } + } + + class.validate_members(context); +} + +/// Check that a `@final` class does not have unimplemented abstract methods. +/// +/// A final class cannot be subclassed, so if it inherits abstract methods without +/// implementing them, those methods can never be implemented, making the class +/// effectively broken. +fn check_final_class_abstract_methods<'db>( + context: &InferContext<'db, '_>, + class: StaticClassLiteral<'db>, + class_node: &ast::StmtClassDef, +) { + let db = context.db(); + + // Only check if the class is final. + if !class.is_final(db) { + return; + } + + // Exclude `Protocol` classes. It is possible to subtype a `Protocol` class + // without subclassing it, so an `@final` `Protocol` class with unimplemented abstract + // methods is not inherently broken in the same way as a non-`Protocol` final class + // with unimplemented abstract methods. + if class.is_protocol(db) { + return; + } + + let class_type = class.identity_specialization(db); + let abstract_methods = class_type.abstract_methods(db); + + // If there are no abstract methods, we're done. + let Some((first_method_name, abstract_method)) = abstract_methods.iter().next() else { + return; + }; + + let Some(builder) = context.report_lint(&ABSTRACT_METHOD_IN_FINAL_CLASS, &class_node.name) + else { + return; + }; + + let class_name = class.name(db); + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Final class `{class_name}` has unimplemented abstract methods", + )); + + let num_abstract_methods = abstract_methods.len(); + + if num_abstract_methods == 1 { + diagnostic.set_concise_message(format_args!( + "Final class `{class_name}` has unimplemented abstract method \ + `{first_method_name}`", + )); + diagnostic.set_primary_message(format_args!("`{first_method_name}` is unimplemented")); + } else { + let verbose = db.verbose(); + let max_abstract_methods_to_print = if verbose { num_abstract_methods } else { 3 }; + let formatted_methods = + format_enumeration(abstract_methods.keys().take(max_abstract_methods_to_print)); + + if num_abstract_methods > max_abstract_methods_to_print { + diagnostic.set_primary_message(format_args!( + "{num_abstract_methods} abstract methods are unimplemented, \ + including {formatted_methods}", + )); + diagnostic.set_concise_message(format_args!( + "Final class `{class_name}` has {num_abstract_methods} unimplemented \ + abstract methods, including {formatted_methods}", + )); + diagnostic.info(format_args!( + "Use `--verbose` to see all {num_abstract_methods} \ + unimplemented abstract methods", + )); + } else { + diagnostic.set_concise_message(format_args!( + "Final class `{class_name}` has unimplemented \ + abstract methods {formatted_methods}", + )); + diagnostic.set_primary_message(format_args!( + "Abstract methods {formatted_methods} are unimplemented" + )); + } + } + + let AbstractMethod { + defining_class, + definition, + kind, + } = abstract_method; + + let module = parsed_module(db, definition.file(db)).load(db); + let span = Span::from(definition.focus_range(db, &module)); + let defining_class_name = defining_class.name(db); + + let mut secondary_annotation = Annotation::secondary(span); + secondary_annotation = if defining_class.class_literal(db) == ClassLiteral::Static(class) { + secondary_annotation.message(format_args!("`{first_method_name}` declared as abstract")) + } else { + secondary_annotation.message(format_args!( + "`{first_method_name}` declared as abstract on superclass `{defining_class_name}`", + )) + }; + diagnostic.annotate(secondary_annotation); + + if !kind.is_explicit() { + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "`{defining_class_name}.{first_method_name}` is implicitly abstract \ + because `{defining_class_name}` is a `Protocol` class \ + and `{first_method_name}` lacks an implementation", + ), + ); + sub.annotate( + Annotation::secondary(defining_class.definition_span(db)) + .message(format_args!("`{defining_class_name}` declared here")), + ); + diagnostic.sub(sub); + + // If the implicitly abstract method is defined in first-party code + // and the return type is assignable to `None`, they may not have intended + // for it to be implicitly abstract; add a clarificatory note: + if kind.is_implicit_due_to_stub_body() && db.should_check_file(definition.file(db)) { + let function_type_as_callable = infer_definition_types(db, *definition) + .binding_type(*definition) + .try_upcast_to_callable(db); + + if let Some(callables) = function_type_as_callable + && Type::function_like_callable( + db, + Signature::new(Parameters::gradual_form(), Type::none(db)), + ) + .is_assignable_to(db, callables.into_type(db)) + { + diagnostic.help(format_args!( + "Change the body of `{first_method_name}` to `return` \ + or `return None` if it was not intended to be abstract" + )); + } + } + } +} + +/// Check for `Final`-qualified declarations in a class body scope that are never +/// assigned a value. +fn check_class_final_without_value<'db>( + context: &InferContext<'db, '_>, + class: StaticClassLiteral<'db>, + index: &SemanticIndex<'db>, +) { + // In stub files, bare declarations without values are normal. + if context.in_stub() { + return; + } + + let db = context.db(); + let body_scope = class.body_scope(db); + let body_scope_id = body_scope.file_scope_id(db); + let use_def = index.use_def_map(body_scope_id); + let place_table = index.place_table(body_scope_id); + + // In dataclasses (and similar code-generated classes), Final fields without + // defaults are initialized by the synthesized __init__, so they are valid. + if CodeGeneratorKind::from_class(db, class.into(), None).is_some() { + return; + } + + for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { + let result = place_from_declarations(db, declarations); + let first_declaration = result.first_declaration; + let (place_and_quals, _) = result.into_place_and_conflicting_declarations(); + + if !place_and_quals.qualifiers.contains(TypeQualifiers::FINAL) { + continue; + } + + // Check if the symbol has any bindings at class level. + let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); + let binding_place = place_from_bindings(db, bindings); + + if !binding_place.place.is_undefined() { + continue; + } + + // Per the typing spec, a `Final` attribute declared in a class body without a + // value must be initialized in `__init__`. Assignments in other methods don't count. + let symbol = place_table.symbol(symbol_id); + if has_binding_in_init(context, body_scope, index, symbol.name().as_str()) { + continue; + } + + let place = place_table.place(symbol_id); + if let Some(first_decl) = first_declaration + && let Some(builder) = context.report_lint( + &FINAL_WITHOUT_VALUE, + first_decl.full_range(db, context.module()), + ) + { + builder.into_diagnostic(format_args!( + "`Final` symbol `{place}` is not assigned a value" + )); + } + } +} + +/// Returns `true` if `name` has any attribute assignment (`self. = ...`) in an +/// `__init__` method of the class whose body scope is `class_body_scope`. +fn has_binding_in_init<'db>( + context: &InferContext<'db, '_>, + class_body_scope: ScopeId<'db>, + index: &SemanticIndex<'db>, + name: &str, +) -> bool { + let db = context.db(); + attribute_assignments(db, class_body_scope, name).any(|(bindings, scope_id)| { + let is_init = index + .scope(scope_id) + .node() + .as_function() + .is_some_and(|f| f.node(context.module()).name.id == "__init__"); + is_init + && bindings + .into_iter() + .any(|b| b.binding.definition().is_some()) + }) +} diff --git a/crates/ty_python_semantic/src/types/infer/deferred/typeguard.rs b/crates/ty_python_semantic/src/types/infer/deferred/typeguard.rs new file mode 100644 index 0000000000000..36d2f92d73432 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/deferred/typeguard.rs @@ -0,0 +1,76 @@ +use ruff_python_ast as ast; + +use crate::{ + semantic_index::SemanticIndex, + types::{Type, context::InferContext, diagnostic::INVALID_TYPE_GUARD_DEFINITION}, +}; + +/// Check that all type guard function definitions have at least one positional parameter +/// (in addition to `self`/`cls` for methods), and for `TypeIs`, that the narrowed type is +/// assignable to the declared type of that parameter. +pub(crate) fn check_type_guard_definition<'db>( + context: &InferContext<'db, '_>, + ty: Type<'db>, + node: &ast::StmtFunctionDef, + index: &SemanticIndex<'db>, +) { + let Type::FunctionLiteral(function) = ty else { + return; + }; + + let db = context.db(); + + for overload in function.iter_overloads_and_implementation(db) { + let signature = overload.signature(db); + let return_ty = signature.return_ty; + + // Check if this is a `TypeIs` or `TypeGuard` return type. + let (type_guard_form_name, narrowed_type) = match return_ty { + Type::TypeIs(type_is) => ("TypeIs", Some(type_is.return_type(db))), + Type::TypeGuard(_) => ("TypeGuard", None), + _ => continue, + }; + + // The return type annotation must exist since we matched `TypeIs`/`TypeGuard`. + let Some(returns_expr) = node.returns.as_deref() else { + continue; + }; + + // Check if this is a non-static method (first parameter is implicit `self`/`cls`). + let is_method = index + .class_definition_of_method(overload.body_scope(db).file_scope_id(db)) + .is_some(); + let has_implicit_receiver = is_method && !overload.is_staticmethod(db); + + // Find the first positional parameter to narrow (skip implicit `self`/`cls`). + let positional_params: Vec<_> = signature.parameters().positional().collect(); + let first_narrowed_param_index = usize::from(has_implicit_receiver); + let first_narrowed_param = positional_params.get(first_narrowed_param_index); + + let Some(first_narrowed_param) = first_narrowed_param else { + if let Some(builder) = context.report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr) + { + builder.into_diagnostic(format_args!( + "`{type_guard_form_name}` function must have a parameter to narrow" + )); + } + continue; + }; + + // For `TypeIs`, check that the narrowed type is assignable to the parameter type. + if let Some(narrowed_ty) = narrowed_type { + let param_ty = first_narrowed_param.annotated_type(); + if !narrowed_ty.is_assignable_to(db, param_ty) + && let Some(builder) = + context.report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr) + { + builder.into_diagnostic(format_args!( + "Narrowed type `{narrowed}` is not assignable \ + to the declared parameter type `{param}`", + narrowed = narrowed_ty.display(db), + param = param_ty.display(db) + )); + } + } + } +} From 69c23cc5a3a6cb08d81b01c7d1c2ba0482c3a3b1 Mon Sep 17 00:00:00 2001 From: Will Duke <41601410+WillDuke@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:20:22 +0000 Subject: [PATCH 213/261] [ty] Render all changed diagnostics in conformance.py (#23613) Co-authored-by: Alex Waygood --- scripts/conformance.py | 850 +++++++++++++++++++++++++---------------- 1 file changed, 526 insertions(+), 324 deletions(-) diff --git a/scripts/conformance.py b/scripts/conformance.py index 6e492d1c1b307..3cf59bc6edbee 100644 --- a/scripts/conformance.py +++ b/scripts/conformance.py @@ -22,9 +22,6 @@ # Custom test directory %(prog)s --target-path custom/tests --old-ty uvx ty@0.0.1a35 --new-ty uvx ty@0.0.7 - # Show all diagnostics (not just changed ones) - %(prog)s --all --old-ty uvx ty@0.0.1a35 --new-ty uvx ty@0.0.7 - # Show a diff with local paths to the test directory instead of table of links %(prog)s --old-ty uvx ty@0.0.1a35 --new-ty uvx ty@0.0.7 --format diff """ @@ -38,12 +35,11 @@ import subprocess import sys import tomllib +from collections import defaultdict from collections.abc import Sequence, Set as AbstractSet from dataclasses import dataclass from enum import StrEnum, auto -from functools import reduce from itertools import chain, groupby -from operator import attrgetter from pathlib import Path from textwrap import dedent from typing import Any, Literal, Self, assert_never @@ -78,11 +74,42 @@ ) CONFORMANCE_URL = CONFORMANCE_DIR_WITH_README + "tests/{filename}#L{line}" +GITHUB_HEADER = [ + "", + "", + "", + "", + "", + "", +] +GITHUB_FOOTER = ["", "
Test caseDiff
"] +SUMMARY_NOTE = """ + Each test case represents one expected error annotation or a group of annotations + sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics + on the same line count as one. Required annotations (`E`) are true positives when + ty flags the expected location and false negatives when it does not. Optional + annotations (`E?`) are true positives when flagged but true negatives (not false + negatives) when not. Tagged annotations (`E[tag]`) require ty to flag exactly one + of the tagged lines; tagged multi-annotations (`E[tag+]`) allow any number up to + the tag count. Flagging unexpected locations counts as a false positive. +""" +# Priority order for section headings: improvements first, regressions last. +TITLE_PRIORITY: dict[str, int] = { + "True positives added": 0, + "False positives removed": 1, + "True positives changed": 2, + "False positives changed": 3, + "False positives added": 4, + "True positives removed": 5, + "Optional Diagnostics Added": 6, + "Optional Diagnostics Removed": 7, + "Optional Diagnostics Changed": 8, +} + class Source(StrEnum): OLD = auto() NEW = auto() - EXPECTED = auto() class Classification(StrEnum): @@ -91,30 +118,22 @@ class Classification(StrEnum): TRUE_NEGATIVE = auto() FALSE_NEGATIVE = auto() - def into_title(self) -> str: + def into_title(self, *, verb: Literal["added", "removed", "changed"]) -> str: match self: case Classification.TRUE_POSITIVE: - return "True positives added" + return f"True positives {verb}" case Classification.FALSE_POSITIVE: - return "False positives added" + return f"False positives {verb}" case Classification.TRUE_NEGATIVE: - return "False positives removed" + return f"True negatives {verb}" case Classification.FALSE_NEGATIVE: - return "True positives removed" - - -@dataclass(kw_only=True, slots=True) -class Evaluation: - classification: Classification - true_positives: int = 0 - false_positives: int = 0 - true_negatives: int = 0 - false_negatives: int = 0 + return f"False negatives {verb}" class Change(StrEnum): ADDED = auto() REMOVED = auto() + CHANGED = auto() UNCHANGED = auto() def into_title(self) -> str: @@ -123,6 +142,8 @@ def into_title(self) -> str: return "Optional Diagnostics Added" case Change.REMOVED: return "Optional Diagnostics Removed" + case Change.CHANGED: + return "Optional Diagnostics Changed" case Change.UNCHANGED: return "Optional Diagnostics Unchanged" @@ -146,28 +167,21 @@ class Location: def as_link(self) -> str: file = self.path.name - link = CONFORMANCE_URL.format( - conformance_suite_commit=CONFORMANCE_SUITE_COMMIT, - filename=file, - line=self.positions.begin.line, - ) + link = CONFORMANCE_URL.format(filename=file, line=self.positions.begin.line) return f"[{file}:{self.positions.begin.line}:{self.positions.begin.column}]({link})" @dataclass(kw_only=True, slots=True) -class Diagnostic: +class TyDiagnostic: + """A diagnostic emitted by a ty version (old or new) during a conformance run.""" + check_name: str description: str severity: str location: Location source: Source - optional: bool - # tag identifying an error that can occur on multiple lines - tag: str | None - # True if one or more errors can occur on lines with the same tag - multi: bool - def __post_init__(self, *args, **kwargs) -> None: + def __post_init__(self) -> None: # Remove check name prefix from description self.description = self.description.replace(f"{self.check_name}: ", "") # Escape pipe characters for GitHub markdown tables @@ -204,194 +218,182 @@ def from_gitlab_output( ), ), source=source, - optional=False, - tag=None, - multi=False, ) + @property + def severity_for_display(self) -> str: + return { + "major": "error", + "minor": "warning", + }.get(self.severity, "unknown") + + +@dataclass(kw_only=True, slots=True) +class ExpectedError: + """An error annotation parsed from a conformance test file (e.g. ``# E: ...``).""" + + description: str + location: Location + optional: bool + # tag identifying an error that can occur on multiple lines + tag: str | None + # True if one or more errors can occur on lines with the same tag + multi: bool + @property def key(self) -> str: - """Key to group diagnostics by path and beginning line or path and tag.""" + """Key to group expected errors by path and beginning line or path and tag.""" return ( f"{self.location.path.as_posix()}:{self.location.positions.begin.line}" if self.tag is None else f"{self.location.path.as_posix()}:{self.tag}" ) - @property - def severity_for_display(self) -> str: - return { - "major": "error", - "minor": "warning", - }.get(self.severity, "unknown") + +def diagnostics_are_equivalent(a: list[TyDiagnostic], b: list[TyDiagnostic]) -> bool: + """Compare two diagnostic lists for equality, ignoring the ``source`` field.""" + + def fingerprint(d: TyDiagnostic) -> tuple: + return ( + d.check_name, + d.description, + d.severity, + str(d.location.path), + d.location.positions.begin.line, + d.location.positions.begin.column, + ) + + return sorted(map(fingerprint, a)) == sorted(map(fingerprint, b)) @dataclass(kw_only=True, slots=True) -class GroupedDiagnostics: +class TestCase: key: str - sources: AbstractSet[Source] - old: list[Diagnostic] - new: list[Diagnostic] - expected: list[Diagnostic] + old: list[TyDiagnostic] + new: list[TyDiagnostic] + expected: list[ExpectedError] @property def change(self) -> Change: - if Source.NEW in self.sources and Source.OLD not in self.sources: + if self.new and not self.old: return Change.ADDED - elif Source.OLD in self.sources and Source.NEW not in self.sources: + elif self.old and not self.new: return Change.REMOVED + elif ( + self.old and self.new and not diagnostics_are_equivalent(self.old, self.new) + ): + return Change.CHANGED else: return Change.UNCHANGED @property def optional(self) -> bool: - return bool(self.expected) and all( - diagnostic.optional for diagnostic in self.expected - ) + return bool(self.expected) and all(e.optional for e in self.expected) @property def multi(self) -> bool: - return bool(self.expected) and all( - diagnostic.multi for diagnostic in self.expected - ) + return bool(self.expected) and all(e.multi for e in self.expected) + + @property + def path(self) -> Path: + """Return the source file path for this test case.""" + for diags in (self.new, self.old, self.expected): + if diags: + return diags[0].location.path + raise ValueError(f"No diagnostics in test case {self.key}") - def diagnostics_by_source(self, source: Source) -> list[Diagnostic]: - match source: - case Source.NEW: - return self.new - case Source.OLD: - return self.old - case Source.EXPECTED: - return self.expected + def diagnostics_by_source(self, source: Source) -> list[TyDiagnostic]: + return self.old if source == Source.OLD else self.new - def classify(self, source: Source) -> Evaluation: + def classify(self, source: Source) -> Classification: diagnostics = self.diagnostics_by_source(source) - if source in self.sources: + if diagnostics: if self.optional: - return Evaluation( - classification=Classification.TRUE_POSITIVE, - true_positives=len(diagnostics), - false_positives=0, - true_negatives=0, - false_negatives=0, - ) - - if Source.EXPECTED in self.sources: + return Classification.TRUE_POSITIVE + if self.expected: distinct_lines = len( - { - diagnostic.location.positions.begin.line - for diagnostic in diagnostics - } + {d.location.positions.begin.line for d in diagnostics} ) expected_max = len(self.expected) if self.multi else 1 - if 1 <= distinct_lines <= expected_max: - return Evaluation( - classification=Classification.TRUE_POSITIVE, - true_positives=len(diagnostics), - false_positives=0, - true_negatives=0, - false_negatives=0, - ) + return Classification.TRUE_POSITIVE else: - # We select the line with the most diagnostics - # as our true positive, while the rest are false positives - # TODO: The ty diagnostics below are because we are not - # inferring a precise type for the `key` lambda, which gives - # us the wrong type for `max_line`. - # https://github.com/astral-sh/ty/issues/181 - max_line = max( - groupby( - diagnostics, key=lambda d: d.location.positions.begin.line - ), - key=lambda x: len(x[1]), - ) - remaining = len(diagnostics) - max_line # ty: ignore[unsupported-operator] - # We can never exceed the number of distinct lines - # if the diagnostic is multi, so we ignore that case - return Evaluation( - classification=Classification.FALSE_POSITIVE, - true_positives=max_line, # ty: ignore[invalid-argument-type] - false_positives=remaining, - true_negatives=0, - false_negatives=0, - ) + return Classification.FALSE_POSITIVE else: - return Evaluation( - classification=Classification.FALSE_POSITIVE, - true_positives=0, - false_positives=len(diagnostics), - true_negatives=0, - false_negatives=0, - ) + return Classification.FALSE_POSITIVE - elif Source.EXPECTED in self.sources: + elif self.expected: if self.optional: - return Evaluation( - classification=Classification.TRUE_NEGATIVE, - true_positives=0, - false_positives=0, - true_negatives=len(diagnostics), - false_negatives=0, - ) - return Evaluation( - classification=Classification.FALSE_NEGATIVE, - true_positives=0, - false_positives=0, - true_negatives=0, - false_negatives=1, - ) + return Classification.TRUE_NEGATIVE + return Classification.FALSE_NEGATIVE else: - return Evaluation( - classification=Classification.TRUE_NEGATIVE, - true_positives=0, - false_positives=0, - true_negatives=1, - false_negatives=0, - ) + return Classification.TRUE_NEGATIVE - def _render_row(self, diagnostics: list[Diagnostic]): - locs = [] - check_names = [] - descriptions = [] - for diagnostic in diagnostics: - loc = ( - diagnostic.location.as_link() - if diagnostic.location - else f"`{diagnostic.tag}`" +def render_html_diff_row(tc: TestCase, *, source: Source | None) -> list[str]: + """Render a single HTML with a test-case link and a markdown diff block.""" + all_diags = tc.old + tc.new if source is None else tc.diagnostics_by_source(source) + + if all_diags: + min_line = min(d.location.positions.begin.line for d in all_diags) + max_line = max(d.location.positions.begin.line for d in all_diags) + filename = all_diags[0].location.path.name + if min_line == max_line: + url = CONFORMANCE_URL.format(filename=filename, line=min_line) + display = f"{filename}:{min_line}" + else: + url = ( + f"{CONFORMANCE_DIR_WITH_README}tests/{filename}#L{min_line}-L{max_line}" ) - locs.append(loc) - check_names.append(diagnostic.check_name) - descriptions.append(diagnostic.description) - - return f"| {'
'.join(locs)} | {'
'.join(check_names)} | {'
'.join(descriptions)} |" - - def _render_diff(self, diagnostics: list[Diagnostic], *, removed: bool = False): - sign = "-" if removed else "+" - return "\n".join(f"{sign} {diagnostic}" for diagnostic in diagnostics) - - def display(self, format: Literal["diff", "github"]) -> str: - eval = self.classify(Source.NEW) - match eval.classification: - case Classification.TRUE_POSITIVE | Classification.FALSE_POSITIVE: - assert self.new is not None - return ( - self._render_diff(self.new) - if format == "diff" - else self._render_row(self.new) - ) + display = f"{filename}:{min_line}:{max_line}" + location = f"[{display}]({url})" + else: + location = tc.key - case Classification.FALSE_NEGATIVE | Classification.TRUE_NEGATIVE: - diagnostics = self.old if self.old else self.expected + diff_lines = [] + if source is None: + for d in tc.old: + diff_lines.append( + f"-{d.severity_for_display}[{d.check_name}] {d.description}" + ) + for d in tc.new: + diff_lines.append( + f"+{d.severity_for_display}[{d.check_name}] {d.description}" + ) + else: + sign = "-" if source == Source.OLD else "+" + for d in tc.diagnostics_by_source(source): + diff_lines.append( + f"{sign}{d.severity_for_display}[{d.check_name}] {d.description}" + ) - return ( - self._render_diff(diagnostics, removed=True) - if format == "diff" - else self._render_row(diagnostics) - ) + return [ + "", + "", + "", + "", + "", + location, + "", + "", + "", + "", + "", + "```diff", + *diff_lines, + "```", + "", + "", + "", + "", + ] + + +def render_diff_row(diagnostics: list[TyDiagnostic], *, removed: bool = False) -> str: + sign = "-" if removed else "+" + return "\n".join(f"{sign} {d}" for d in diagnostics) @dataclass(kw_only=True, slots=True) @@ -415,42 +417,72 @@ def recall(self) -> float: return 0.0 -def collect_expected_diagnostics(test_files: Sequence[Path]) -> list[Diagnostic]: - diagnostics: list[Diagnostic] = [] +@dataclass(kw_only=True, slots=True) +class DiagnosticEntry: + """A test case bound to the section title and source side it should be rendered under.""" + + title: str + test_case: TestCase + # None means show both old (removed) and new (added) in a single "changed" section. + source: Source | None + + +@dataclass(kw_only=True, slots=True) +class FileStats: + path: Path + old: Statistics + new: Statistics + + @property + def old_passes(self) -> bool: + return self.old.false_positives == 0 and self.old.false_negatives == 0 + + @property + def new_passes(self) -> bool: + return self.new.false_positives == 0 and self.new.false_negatives == 0 + + @property + def total_change(self) -> int: + return ( + abs(self.new.true_positives - self.old.true_positives) + + abs(self.new.false_positives - self.old.false_positives) + + abs(self.new.false_negatives - self.old.false_negatives) + ) + + +def collect_expected_diagnostics(test_files: Sequence[Path]) -> list[ExpectedError]: + errors: list[ExpectedError] = [] for file in test_files: for idx, line in enumerate(file.read_text().splitlines(), 1): - if error := re.search(CONFORMANCE_ERROR_PATTERN, line): - diagnostics.append( - Diagnostic( - check_name="conformance", - description=(error.group("description") or "Missing"), - severity="major", + if match := re.search(CONFORMANCE_ERROR_PATTERN, line): + errors.append( + ExpectedError( + description=(match.group("description") or "Missing"), location=Location( path=file, positions=Positions( begin=Position( line=idx, - column=error.start(), + column=match.start(), ), end=Position( line=idx, - column=error.end(), + column=match.end(), ), ), ), - source=Source.EXPECTED, - optional=error.group("optional") is not None, + optional=match.group("optional") is not None, tag=( - f"{file.name}:{error.group('tag')}" - if error.group("tag") + f"{file.name}:{match.group('tag')}" + if match.group("tag") else None ), - multi=error.group("multi") is not None, + multi=match.group("multi") is not None, ) ) - assert diagnostics, "Failed to discover any expected diagnostics!" - return diagnostics + assert errors, "Failed to discover any expected diagnostics!" + return errors def collect_ty_diagnostics( @@ -459,7 +491,7 @@ def collect_ty_diagnostics( test_files: Sequence[Path], python_version: str = "3.12", extra_search_paths: Sequence[Path] = (), -) -> list[Diagnostic]: +) -> list[TyDiagnostic]: extra_search_path_args = [ f"--extra-search-path={path}" for path in extra_search_paths ] @@ -484,146 +516,307 @@ def collect_ty_diagnostics( timeout=15, ) - if process.returncode != 0: - print(process.stderr) - raise RuntimeError(f"ty check failed with exit code {process.returncode}") - return [ - Diagnostic.from_gitlab_output(dct, source=source) + TyDiagnostic.from_gitlab_output(dct, source=source) for dct in json.loads(process.stdout) if dct["severity"] == "major" ] def group_diagnostics_by_key( - old: list[Diagnostic], - new: list[Diagnostic], - expected: list[Diagnostic], -) -> list[GroupedDiagnostics]: - # propagate tags from expected diagnostics to old and new diagnostics - tagged_lines = { - (d.location.path.name, d.location.positions.begin.line): d.tag - for d in expected - if d.tag is not None + old: list[TyDiagnostic], + new: list[TyDiagnostic], + expected: list[ExpectedError], +) -> list[TestCase]: + # Build a lookup from (filename, line) to tag so ty diagnostics on a tagged + # line can be grouped with all other expected errors sharing that tag. + tagged_lines: dict[tuple[str, int], str] = { + (e.location.path.name, e.location.positions.begin.line): e.tag + for e in expected + if e.tag is not None } - for diag in chain(old, new): - diag.tag = tagged_lines.get( - ( - diag.location.path.name, - diag.location.positions.begin.line, - ) + def ty_key(diag: TyDiagnostic) -> str: + tag = tagged_lines.get( + (diag.location.path.name, diag.location.positions.begin.line) + ) + return ( + f"{diag.location.path.as_posix()}:{tag}" + if tag is not None + else f"{diag.location.path.as_posix()}:{diag.location.positions.begin.line}" ) - diagnostics = [ - *old, - *new, - *expected, - ] + old_by_key: defaultdict[str, list[TyDiagnostic]] = defaultdict(list) + new_by_key: defaultdict[str, list[TyDiagnostic]] = defaultdict(list) + expected_by_key: defaultdict[str, list[ExpectedError]] = defaultdict(list) + + for diag in old: + old_by_key[ty_key(diag)].append(diag) + for diag in new: + new_by_key[ty_key(diag)].append(diag) + for err in expected: + expected_by_key[err.key].append(err) - diagnostics = sorted(diagnostics, key=attrgetter("key")) - grouped_diagnostics = [] - for key, group in groupby(diagnostics, key=attrgetter("key")): - old_diagnostics: list[Diagnostic] = [] - new_diagnostics: list[Diagnostic] = [] - expected_diagnostics: list[Diagnostic] = [] - sources: set[Source] = set() - - for diag in group: - sources.add(diag.source) - match diag.source: - case Source.OLD: - old_diagnostics.append(diag) - case Source.NEW: - new_diagnostics.append(diag) - case Source.EXPECTED: - expected_diagnostics.append(diag) - - grouped = GroupedDiagnostics( + all_keys = sorted(old_by_key.keys() | new_by_key.keys() | expected_by_key.keys()) + return [ + TestCase( key=key, - sources=sources, - old=old_diagnostics, - new=new_diagnostics, - expected=expected_diagnostics, + old=old_by_key[key], + new=new_by_key[key], + expected=expected_by_key[key], ) - grouped_diagnostics.append(grouped) + for key in all_keys + ] - return grouped_diagnostics +def compute_stats(test_cases: list[TestCase], source: Source) -> Statistics: + stats = Statistics() + for tc in test_cases: + match tc.classify(source): + case Classification.TRUE_POSITIVE: + stats.true_positives += 1 + case Classification.FALSE_POSITIVE: + stats.false_positives += 1 + case Classification.FALSE_NEGATIVE: + stats.false_negatives += 1 + case Classification.TRUE_NEGATIVE: + pass + stats.total_diagnostics += len(tc.diagnostics_by_source(source)) + return stats + + +def collect_diagnostic_entries(test_cases: list[TestCase]) -> list[DiagnosticEntry]: + """Classify each changed test case and assign it to a titled section.""" + entries: list[DiagnosticEntry] = [] -def compute_stats( - grouped_diagnostics: list[GroupedDiagnostics], - ty_version: Literal["new", "old"], -) -> Statistics: - source = Source.NEW if ty_version == "new" else Source.OLD + for tc in test_cases: + change = tc.change + if change == Change.UNCHANGED: + continue - def increment(statistics: Statistics, grouped: GroupedDiagnostics) -> Statistics: - eval = grouped.classify(source) - statistics.true_positives += eval.true_positives - statistics.false_positives += eval.false_positives - statistics.false_negatives += eval.false_negatives - statistics.total_diagnostics += len(grouped.diagnostics_by_source(source)) - return statistics + if tc.optional: + if change == Change.ADDED: + entries.append( + DiagnosticEntry( + title=Change.ADDED.into_title(), test_case=tc, source=Source.NEW + ) + ) + elif change == Change.REMOVED: + entries.append( + DiagnosticEntry( + title=Change.REMOVED.into_title(), + test_case=tc, + source=Source.OLD, + ) + ) + elif change == Change.CHANGED: + entries.append( + DiagnosticEntry( + title=Change.CHANGED.into_title(), test_case=tc, source=None + ) + ) + else: + if change == Change.ADDED: + new_class = tc.classify(Source.NEW) + entries.append( + DiagnosticEntry( + title=new_class.into_title(verb="added"), + test_case=tc, + source=Source.NEW, + ) + ) + elif change == Change.REMOVED: + old_class = tc.classify(Source.OLD) + entries.append( + DiagnosticEntry( + title=old_class.into_title(verb="removed"), + test_case=tc, + source=Source.OLD, + ) + ) + elif change == Change.CHANGED: + old_class = tc.classify(Source.OLD) + new_class = tc.classify(Source.NEW) + if old_class == new_class: + # Same classification but different diagnostics: one "changed" section. + entries.append( + DiagnosticEntry( + title=new_class.into_title(verb="changed"), + test_case=tc, + source=None, + ) + ) + else: + # Classification changed: split into separate removed/added sections. + entries.append( + DiagnosticEntry( + title=old_class.into_title(verb="removed"), + test_case=tc, + source=Source.OLD, + ) + ) + entries.append( + DiagnosticEntry( + title=new_class.into_title(verb="added"), + test_case=tc, + source=Source.NEW, + ) + ) - return reduce(increment, grouped_diagnostics, Statistics()) + entries.sort( + key=lambda e: (TITLE_PRIORITY.get(e.title, 99), e.title, e.test_case.key) + ) + return entries -def render_grouped_diagnostics( - grouped: list[GroupedDiagnostics], +def render_test_cases( + test_cases: list[TestCase], *, - changed_only: bool = True, format: Literal["diff", "github"] = "diff", ) -> str: - if changed_only: - grouped = [ - diag for diag in grouped if diag.change in (Change.ADDED, Change.REMOVED) - ] + entries = collect_diagnostic_entries(test_cases) + if not entries: + return "" - get_change = attrgetter("change") + lines = [] + for title, group in groupby(entries, key=lambda e: e.title): + group_list = list(group) + n = len(group_list) + + lines.append(f"### {title} ({n})") + lines.extend( + [ + "", + "
", + f"{n} {'diagnostic' if n == 1 else 'diagnostics'}", + "", + ] + ) - def get_classification(diag) -> Classification: - return diag.classify(Source.NEW).classification + if format == "diff": + lines.append("```diff") + else: + lines.extend(GITHUB_HEADER) + + for entry in group_list: + tc, source = entry.test_case, entry.source + if source is None: + if format == "diff": + lines.append(render_diff_row(tc.old, removed=True)) + lines.append(render_diff_row(tc.new, removed=False)) + else: + lines.extend(render_html_diff_row(tc, source=None)) + else: + if format == "diff": + lines.append( + render_diff_row( + tc.diagnostics_by_source(source), + removed=source == Source.OLD, + ) + ) + else: + lines.extend(render_html_diff_row(tc, source=source)) - optional_diagnostics = sorted( - (diag for diag in grouped if diag.optional), - key=get_change, - reverse=True, - ) - required_diagnostics = sorted( - (diag for diag in grouped if not diag.optional), - key=get_classification, - reverse=True, - ) + if format == "diff": + lines.append("```") + else: + lines.extend(GITHUB_FOOTER) + lines.extend(["", "
", ""]) - match format: - case "diff": - header = ["```diff"] - footer = "```" - case "github": - header = [ - "| Location | Name | Message |", - "|----------|------|---------|", - ] - footer = "" - case _: - raise ValueError("format must be one of 'diff' or 'github'") + return "\n".join(lines) - lines = [] - for group, diagnostics in chain( - groupby(required_diagnostics, key=get_classification), - groupby(optional_diagnostics, key=get_change), - ): - lines.append(f"### {group.into_title()}") - lines.extend(["", "
", ""]) - lines.extend(header) +def render_file_stats_table(test_cases: list[TestCase]) -> str: + """Render a per-file breakdown showing only files whose TP/FP/FN counts changed.""" + path_to_cases: dict[Path, list[TestCase]] = {} + for tc in test_cases: + path_to_cases.setdefault(tc.path, []).append(tc) + + def fmt(old: int, new: int, *, greater_is_better: bool = True) -> str: + if old == new: + return str(new) + diff = new - old + improved = diff > 0 if greater_is_better else diff < 0 + indicator = " ✅" if improved else " ❌" + return f"{new} ({diff:+}){indicator}" + + # Collect per-file data; track totals across all files regardless of change. + file_stats: list[FileStats] = [] + old_totals = Statistics() + new_totals = Statistics() + passing = 0 + total_files = 0 + + for path, cases in path_to_cases.items(): + fs = FileStats( + path=path, + old=compute_stats(cases, Source.OLD), + new=compute_stats(cases, Source.NEW), + ) + old_totals.true_positives += fs.old.true_positives + old_totals.false_positives += fs.old.false_positives + old_totals.false_negatives += fs.old.false_negatives + new_totals.true_positives += fs.new.true_positives + new_totals.false_positives += fs.new.false_positives + new_totals.false_negatives += fs.new.false_negatives + passing += fs.new_passes + total_files += 1 + file_stats.append(fs) + + changed = [fs for fs in file_stats if fs.total_change > 0] + if not changed: + return "" - for diag in diagnostics: - lines.append(diag.display(format=format)) + changed.sort(key=lambda fs: (-fs.total_change, fs.path.name)) - lines.append(footer) - lines.extend(["", "
", ""]) + rows = [] + for fs in changed: + if fs.new_passes and not fs.old_passes: + status = "✅ Newly Passing 🎉" + elif fs.old_passes and not fs.new_passes: + status = "❌ Newly Failing" + elif fs.new_passes: + status = "✅" + else: + old_errors = fs.old.false_positives + fs.old.false_negatives + new_errors = fs.new.false_positives + fs.new.false_negatives + if new_errors < old_errors: + status = "📈 Improving" + elif new_errors > old_errors: + status = "📉 Regressing" + else: + status = "➡️ Neutral" + url = CONFORMANCE_DIR_WITH_README + f"tests/{fs.path.name}" + rows.append( + f"| [{fs.path.name}]({url})" + f" | {fmt(fs.old.true_positives, fs.new.true_positives, greater_is_better=True)}" + f" | {fmt(fs.old.false_positives, fs.new.false_positives, greater_is_better=False)}" + f" | {fmt(fs.old.false_negatives, fs.new.false_negatives, greater_is_better=False)}" + f" | {status} |" + ) + + totals_row = ( + f"| **Total (all files)**" + f" | **{fmt(old_totals.true_positives, new_totals.true_positives, greater_is_better=True)}**" + f" | **{fmt(old_totals.false_positives, new_totals.false_positives, greater_is_better=False)}**" + f" | **{fmt(old_totals.false_negatives, new_totals.false_negatives, greater_is_better=False)}**" + f" | {passing}/{total_files} |" + ) + lines = [ + "### Test file breakdown", + "", + "
", + f"{len(changed)} file{'s' if len(changed) != 1 else ''} altered", + "", + "| File | True Positives | False Positives | False Negatives | Status |", + "|------|----|----|----|--------|", + *rows, + totals_row, + "", + "
", + "", + ] return "\n".join(lines) @@ -658,9 +851,7 @@ def diff_format( assert_never((greater_is_better, increased)) # ty: ignore[type-assertion-failure] -def render_summary( - grouped_diagnostics: list[GroupedDiagnostics], *, force_summary_table: bool -) -> str: +def render_summary(test_cases: list[TestCase], *, force_summary_table: bool) -> str: def format_metric(diff: float, old: float, new: float): if diff > 0: return f"increased from {old:.2%} to {new:.2%}" @@ -668,12 +859,12 @@ def format_metric(diff: float, old: float, new: float): return f"decreased from {old:.2%} to {new:.2%}" return f"held steady at {old:.2%}" - old = compute_stats(grouped_diagnostics, ty_version="old") - new = compute_stats(grouped_diagnostics, ty_version="new") + old = compute_stats(test_cases, Source.OLD) + new = compute_stats(test_cases, Source.NEW) assert new.true_positives > 0, ( "Expected ty to have at least one true positive.\n" - f"Sample of grouped diagnostics: {grouped_diagnostics[:5]}" + f"Sample of grouped diagnostics: {test_cases[:5]}" ) precision_change = new.precision - old.precision @@ -683,16 +874,27 @@ def format_metric(diff: float, old: float, new: float): false_neg_change = new.false_negatives - old.false_negatives total_change = new.total_diagnostics - old.total_diagnostics + summary_paragraph = ( + f"The percentage of diagnostics emitted that were expected errors " + f"{format_metric(precision_change, old.precision, new.precision)}. " + f"The percentage of expected errors that received a diagnostic " + f"{format_metric(recall_change, old.recall, new.recall)}." + ) + base_header = f"[Typing conformance results]({CONFORMANCE_DIR_WITH_README})" if not force_summary_table and all( - diag.change is Change.UNCHANGED for diag in grouped_diagnostics + diag.change is Change.UNCHANGED for diag in test_cases ): return dedent( f""" ## {base_header} - No changes detected ✅ + ### No changes detected ✅ + + --- + + {summary_paragraph} """ ) @@ -714,12 +916,7 @@ def format_metric(diff: float, old: float, new: float): else: header = base_header - summary_paragraph = ( - f"The percentage of diagnostics emitted that were expected errors " - f"{format_metric(precision_change, old.precision, new.precision)}. " - f"The percentage of expected errors that received a diagnostic " - f"{format_metric(recall_change, old.recall, new.recall)}." - ) + summary_note = " ".join(SUMMARY_NOTE.split()) return dedent( f""" @@ -729,6 +926,15 @@ def format_metric(diff: float, old: float, new: float): ### Summary +
+ How are test cases classified? + +
+ + {summary_note} + +
+ | Metric | Old | New | Diff | Outcome | |--------|-----|-----|------|---------| | True Positives | {old.true_positives} | {new.true_positives} | {true_pos_change:+} | {true_pos_diff} | @@ -799,12 +1005,6 @@ def parse_args(): help="Python version to assume when running ty (default: 3.12)", ) - parser.add_argument( - "--all", - action="store_true", - help="Show all diagnostics, not just changed ones", - ) - parser.add_argument( "--format", type=str, choices=["diff", "github"], default="github" ) @@ -859,12 +1059,14 @@ def main(): ) rendered = "\n\n".join( - [ - render_summary(grouped, force_summary_table=args.force_summary_table), - render_grouped_diagnostics( - grouped, changed_only=not args.all, format=args.format - ), - ] + filter( + None, + [ + render_summary(grouped, force_summary_table=args.force_summary_table), + render_file_stats_table(grouped), + render_test_cases(grouped, format=args.format), + ], + ) ) if args.output: From 5e4a3d9c3b381df20f6a52caef0f56ed0ebc74be Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Thu, 5 Mar 2026 11:48:21 -0800 Subject: [PATCH 214/261] Bump 0.15.5 (#23743) Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- CHANGELOG.md | 40 +++++++++++++++++++++++++++++++ Cargo.lock | 6 ++--- README.md | 6 ++--- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/formatter.md | 2 +- docs/integrations.md | 8 +++---- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 11 files changed, 57 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e11124b647a41..33a1cbc4f1bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 0.15.5 + +Released on 2026-03-05. + +### Preview features + +- Discover Markdown files by default in preview mode ([#23434](https://github.com/astral-sh/ruff/pull/23434)) +- \[`perflint`\] Extend `PERF102` to comprehensions and generators ([#23473](https://github.com/astral-sh/ruff/pull/23473)) +- \[`refurb`\] Fix `FURB101` and `FURB103` false positives when I/O variable is used later ([#23542](https://github.com/astral-sh/ruff/pull/23542)) +- \[`ruff`\] Add fix for `none-not-at-end-of-union` (`RUF036`) ([#22829](https://github.com/astral-sh/ruff/pull/22829)) +- \[`ruff`\] Fix false positive for `re.split` with empty string pattern (`RUF055`) ([#23634](https://github.com/astral-sh/ruff/pull/23634)) + +### Bug fixes + +- \[`fastapi`\] Handle callable class dependencies with `__call__` method (`FAST003`) ([#23553](https://github.com/astral-sh/ruff/pull/23553)) +- \[`pydocstyle`\] Fix numpy section ordering (`D420`) ([#23685](https://github.com/astral-sh/ruff/pull/23685)) +- \[`pyflakes`\] Fix false positive for names shadowing re-exports (`F811`) ([#23356](https://github.com/astral-sh/ruff/pull/23356)) +- \[`pyupgrade`\] Avoid inserting redundant `None` elements in `UP045` ([#23459](https://github.com/astral-sh/ruff/pull/23459)) + +### Documentation + +- Document extension mapping for Markdown code formatting ([#23574](https://github.com/astral-sh/ruff/pull/23574)) +- Update default Python version examples ([#23605](https://github.com/astral-sh/ruff/pull/23605)) + +### Other changes + +- Publish releases to Astral mirror ([#23616](https://github.com/astral-sh/ruff/pull/23616)) + +### Contributors + +- [@amyreese](https://github.com/amyreese) +- [@stakeswky](https://github.com/stakeswky) +- [@chirizxc](https://github.com/chirizxc) +- [@anishgirianish](https://github.com/anishgirianish) +- [@bxff](https://github.com/bxff) +- [@zsol](https://github.com/zsol) +- [@charliermarsh](https://github.com/charliermarsh) +- [@ntBre](https://github.com/ntBre) +- [@kar-ganap](https://github.com/kar-ganap) + ## 0.15.4 Released on 2026-02-26. diff --git a/Cargo.lock b/Cargo.lock index b30d76d552183..635b0b37bb0bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3026,7 +3026,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.15.4" +version = "0.15.5" dependencies = [ "anyhow", "argfile", @@ -3289,7 +3289,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.4" +version = "0.15.5" dependencies = [ "aho-corasick", "anyhow", @@ -3663,7 +3663,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.15.4" +version = "0.15.5" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index 3d0be7e8ec40b..0d68ed1420b10 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.15.4/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.15.4/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.15.5/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.15.5/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -186,7 +186,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.4 + rev: v0.15.5 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 1cbad4154b715..23b67c06d0105 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.15.4" +version = "0.15.5" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 44eebf243bf0a..cd04386a0a10a 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.15.4" +version = "0.15.5" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 9924202371aa8..b687dcedf35fa 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.15.4" +version = "0.15.5" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index 4bd968d7f1c3b..2feba07a7e8c1 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -310,7 +310,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.4 + rev: v0.15.5 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/integrations.md b/docs/integrations.md index 0b554298d558f..11d7e73e6af8b 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.15.4-alpine + name: ghcr.io/astral-sh/ruff:0.15.5-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.4 + rev: v0.15.5 hooks: # Run the linter. - id: ruff-check @@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.4 + rev: v0.15.5 hooks: # Run the linter. - id: ruff-check @@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.4 + rev: v0.15.5 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index a1767480e9d62..d6c094de9f66e 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.4 + rev: v0.15.5 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index e32a04f3de6c3..d09842c850472 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.15.4" +version = "0.15.5" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index c4c7e3b729cbe..d321518ef7cc2 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.15.4" +version = "0.15.5" description = "" authors = ["Charles Marsh "] From a33be00689079b6d41a0ef69abd44cfb6268b708 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 5 Mar 2026 15:00:05 -0500 Subject: [PATCH 215/261] [ty] Make inferred specializations line up with source types more better (#23715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I discovered this while looking into some unexpected ecosystem results on https://github.com/astral-sh/ruff/pull/22578. It took a couple of days of wrestling with pi to discover the issue and reduce it to the following example: ```py def f(l: list[tuple[Any | str, Any | str]]) -> None: # was: dict[str | Any, str | Any] # should be: dict[Any | str, Any | str] reveal_type(dict(l)) ``` We are trying to infer a specialization of `dict` when we pass in the `list[tuple[...]]` type an argument. This matches against the constructor overload that takes in an `Iterator[tuple[_KT, _VT]]`. All good so far. As part of doing that, we create the constraint set `(Any | str ≤ _KT) ∧ (Any | str ≤ _VT)`, which has the following structure: ``` <0> (str ≤ _VT@dict) 4/4 ┡━₁ <1> (Any ≤ _VT@dict) 3/3 │ ┡━₁ <2> (str ≤ _KT@dict) 2/2 │ │ ┡━₁ <3> (Any ≤ _KT@dict) 1/1 │ │ │ ┡━₁ always │ │ │ └─₀ never │ │ └─₀ never │ └─₀ never └─₀ never ``` A few interesting things have happened: - Both typevars correctly have lower bound (contravariant) constraints: function parameters are contravariant; `Iterator` is covariant; contra × co = contra - The constraint set lines up with "source order": the type explicitly mentioned in the code is `Any | str`, and `_KT` appears before `_VT` in the definition of `dict`. - We break apart lower bound unions: e.g. `Any | str ≤ _KT` becomes `(Any ≤ _KT) ∧ (str ≤ _KT)` - That gives us four "atomic" constraints, which we correctly create in source order: `Any ≤ _KT`, `str ≤ _KT`, `Any ≤ _VT`, `str ≤ _VT`. (The `1/1` etc in the graph rendering is the `source_order` that we have assigned to each constraint.) - Our BDDs use the _reverse_ of the constraint ID ordering, so the constraint with the smallest ID (`Any ≤ _KT`) appears closest to the leaf nodes. (This is on purpose for efficiency reasons) Everything described so far is correct and expected behavior. Next we have to find a solution for this constraint set. To do that, we find every path from the root to the `always` terminal, remembering all of the constraints that we encounter along that path. In this case, there is only one path: ``` str ≤ _VT (4) Any ≤ _VT (3) str ≤ _KT (2) Any ≤ _KT (1) ``` Those constraints are still in reverse order! But we're tracking a `source_order` for each constraint, and we [sort by `source_order`](https://github.com/astral-sh/ruff/blob/149c5786ca6ae135431b86d1cd8ab74763b9a95a/crates/ty_python_semantic/src/types/constraints.rs#L3042-L3051) before turning this list of constraints into a solution. That _should_ mean that we build up the unions as `Any | str`. So why were we seeing `str | Any` instead? If you print out the `source_order`s that we actually see when we get to the sort, we have: ``` str ≤ _VT (4) Any ≤ _VT (4) str ≤ _KT (2) Any ≤ _KT (2) ``` The `Any` constraints have copied their `source_order`s from the corresponding `str` constraints! What has happened is that some purposeful behavior has engaged too aggressively. When we look at a path that represents a solution, we want to know _all_ of the constraints that are true for that path — not just the ones that are explicitly mentioned in the BDD. We have a `SequentMap` type that records the relationships between constraints. Among other things, it records _implications_: "when X is true, Y is true as well". As we build up potential paths, each time we add one of the constraints from the BDD, we immediately check the sequent map to see if we now know any other _derived_ facts to be true. If so, we add them to the path. And importantly, we want those derived facts to appear "near" their "origin" constraint — so we give the derived constraint the same `source_order` as the BDD constraint we just added. In this case, the first constraint we encounter in the BDD is `str ≤ _VT (4)`. The sequent map tells us that `str ≤ _VT` implies `Any ≤ _VT`, so we add `Any ≤ _VT` as a _derived_ constraint, which inherits `source_order=4` from its origin constraint. Next we encounter `Any ≤ _VT` as an _origin_ constraint (since it appears in the BDD). But since it's already in the path, we don't add it as a duplicate or update its `source_order`. The fix is straightforward: origin `source_order`s should take precedence over inherited derived `source_order`s. --- .../mdtest/generics/pep695/functions.md | 14 ++++++++++++++ .../ty_python_semantic/src/types/constraints.rs | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 57eb1184a0a34..1a624459a4de0 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -1075,5 +1075,19 @@ def g[S: (bool, str)](x: S) -> S: return f(x) # error: [invalid-argument-type] ``` +## Display ordering + +Where possible, we want the types that appear in inferred specializations to line up with the types +that are listed in the source code. We don't want arbitarily reorder e.g. union elements as part of +finding a solution. + +```py +from typing import Any + +def f(l: list[tuple[Any | str, Any | str]]) -> None: + # revealed: dict[Any | str, Any | str] + reveal_type(dict(l)) +``` + [implies_subtype_of]: ../../type_properties/implies_subtype_of.md [ty#2371]: https://github.com/astral-sh/ty/issues/2371 diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 2b011db3b3001..f202659367add 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -4581,7 +4581,7 @@ impl PathAssignments { edge = %assignment.display(db, builder), "walk edge", ); - let found_conflict = self.add_assignment(db, builder, assignment, source_order); + let found_conflict = self.add_assignment(db, builder, assignment, source_order, false); let result = if found_conflict.is_err() { // If that results in the path now being impossible due to a contradiction, return // without invoking the callback. @@ -4662,6 +4662,7 @@ impl PathAssignments { builder: &ConstraintSetBuilder<'db>, assignment: ConstraintAssignment, source_order: usize, + derived: bool, ) -> Result<(), PathAssignmentConflict> { // First add this assignment. If it causes a conflict, return that as an error. If we've // already know this assignment holds, just return. @@ -4682,7 +4683,16 @@ impl PathAssignments { match self.assignments.entry(assignment) { Entry::Vacant(entry) => entry.insert(source_order), - Entry::Occupied(_) => return Ok(()), + Entry::Occupied(mut entry) => { + // If a constraint appears both as an "origin" constraint (it actually appears in + // the BDD structure) and as a "derived" constraint (we infer it from other + // constraints), we should prefer the origin source_order, regardless of which + // order we encounter the various constraints in the BDD. + if !derived { + *entry.get_mut() = source_order; + } + return Ok(()); + } }; // Then use our sequents to add additional facts that we know to be true. We currently @@ -4759,7 +4769,7 @@ impl PathAssignments { } for new_constraint in new_constraints { - self.add_assignment(db, builder, new_constraint.when_true(), source_order)?; + self.add_assignment(db, builder, new_constraint.when_true(), source_order, true)?; } Ok(()) From 74b05539720b3d83d5f269eefbfcd929cc60263d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 5 Mar 2026 20:02:04 +0000 Subject: [PATCH 216/261] conformance.py: Collapse the summary paragraph when nothing changed (#23745) --- scripts/conformance.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/conformance.py b/scripts/conformance.py index 3cf59bc6edbee..a7b83a4e690d5 100644 --- a/scripts/conformance.py +++ b/scripts/conformance.py @@ -892,9 +892,13 @@ def format_metric(diff: float, old: float, new: float): ### No changes detected ✅ - --- +
+ Current numbers +
{summary_paragraph} + +
""" ) From f9324a5bf669da13b1c3b33e01881bc670b3c3c7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 5 Mar 2026 20:04:08 +0000 Subject: [PATCH 217/261] Update conformance suite commit hash (#23746) Co-authored-by: Claude --- .github/workflows/typing_conformance.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index 6ff5d7b8572d9..6613652aabea3 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -34,7 +34,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 RUST_BACKTRACE: 1 - CONFORMANCE_SUITE_COMMIT: 56b7944b90d428d7014b4550452c2a187c70f482 + CONFORMANCE_SUITE_COMMIT: 7f8f6bbcaac9be6cde08c886f32aab64a3d597d2 PYTHON_VERSION: 3.12 jobs: From d2479057a231e227d9661df8dbf7a477f07db913 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Mar 2026 16:59:47 -0500 Subject: [PATCH 218/261] [ty] Materialize only substituted typevars during specialization (#23725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary See https://github.com/astral-sh/ty/issues/2955#issuecomment-4000172171 for a comprehensive write-up of the underlying issue. Here's a Claude-generated example: ```python from typing import Any class InvariantWithAny[T: int]: a: T b: Any def _(x: object): if isinstance(x, InvariantWithAny): # x is narrowed to InvariantWithAny[Unknown], then materialized. # T was substituted (T → Unknown → object), so `a` becomes `object`. # But `b` was annotated as `Any` by the user — it has nothing to do with T. reveal_type(x.a) # revealed: object ← correct either way reveal_type(x.b) # revealed: Any ← fixed (was: object) ``` On main, the second pass materializes every gradual type, so `Any` on `b` was turned into `object`. Now, only the types that come from substituting `T` gets materialized, so the explicit `Any` on `b` is left alone. IIUC, this is a bit worse in the contravariant case (again, with Claude's help): ```python class Handler[T]: def handle(self, event: T, context: Any) -> None: ... def _(x: object): if isinstance(x, Handler): # handle(event: Never, context: Never) ← bug # handle(event: Never, context: Any) ← fixed x.handle(42, {"key": "value"}) ``` Closes https://github.com/astral-sh/ty/issues/2955. --- .../resources/mdtest/narrow/isinstance.md | 87 +++++++++++++++++++ crates/ty_python_semantic/src/place.rs | 14 +-- crates/ty_python_semantic/src/types.rs | 54 +++++++----- crates/ty_python_semantic/src/types/class.rs | 12 +-- .../src/types/known_instance.rs | 1 + .../src/types/signatures.rs | 5 +- .../src/types/subclass_of.rs | 14 +-- .../ty_python_semantic/src/types/typevar.rs | 57 ++++++++---- 8 files changed, 173 insertions(+), 71 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index b44f28efde8ac..d8c0e891dba98 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -565,6 +565,23 @@ def _(x: object): x.push(42) ``` +The same applies when the contravariant type parameter appears inside `type[T]`: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T", contravariant=True) + +class ContravariantType(Generic[T]): + def push(self, x: type[T]) -> None: ... + +def _(x: object): + if isinstance(x, ContravariantType): + reveal_type(x) # revealed: ContravariantType[Never] + # error: [invalid-argument-type] + x.push(str) +``` + Invariant generics are trickiest. The top materialization, conceptually the type that includes all instances of the generic class regardless of the type parameter, cannot be represented directly in the type system, so we represent it with the internal `Top[]` special form. @@ -583,6 +600,39 @@ def _(x: object): x.push(42) ``` +When reading attributes from a top-materialized generic, only type parameters should be +materialized. Unrelated gradual attribute types should be preserved. + +```py +from typing import Any + +class InvariantWithAny[T: int]: + a: T + b: Any + +def _(x: object): + if isinstance(x, InvariantWithAny): + reveal_type(x) # revealed: Top[InvariantWithAny[Unknown]] + reveal_type(x.a) # revealed: object + reveal_type(x.b) # revealed: Any +``` + +The same applies in contravariant positions: `Any` in a parameter type that isn't tied to the +generic parameter should not be materialized. + +```py +from typing import Any + +class ContravariantWithAny[T]: + def push(self, x: T, y: Any) -> None: ... + +def _(x: object): + if isinstance(x, ContravariantWithAny): + reveal_type(x) # revealed: ContravariantWithAny[Never] + # error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`" + x.push(42, "hello") +``` + When more complex types are involved, the `Top[]` type may get simplified away. ```py @@ -615,6 +665,43 @@ def _(x: type[object], y: type[object], z: type[object]): reveal_type(z) # revealed: type[Top[Invariant[Unknown]]] ``` +## Narrowing generic defaults in Python 3.13 + +When a type parameter has a bare `Any` default, narrowing still materializes the substituted +typevar. The default isn't used during `isinstance` narrowing (the type parameter gets `Unknown` +instead), so the default value is irrelevant here: + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import Any + +class WithAnyDefault[T = Any]: + y: tuple[Any, T] + +def _(x: object): + if isinstance(x, WithAnyDefault): + reveal_type(x.y) # revealed: tuple[Any, object] +``` + +Type alias defaults substituted into type parameters still need to be materialized when narrowing: + +```py +from typing import Any + +type A = Any + +class WithAliasDefault[T = A]: + y: tuple[A, T] + +def _(x: object): + if isinstance(x, WithAliasDefault): + reveal_type(x.y) # revealed: tuple[A, object] +``` + ## Narrowing with TypedDict unions Narrowing unions of `int` and multiple TypedDicts using `isinstance(x, dict)` should not panic diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 8837cf95d3361..bd9618f93ef30 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -15,9 +15,8 @@ use crate::semantic_index::{ }; use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map}; use crate::types::{ - ApplyTypeMappingVisitor, DynamicType, KnownClass, MaterializationKind, MemberLookupPolicy, - Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, binding_type, - declaration_type, + DynamicType, KnownClass, MemberLookupPolicy, Truthiness, Type, TypeAndQualifiers, + TypeQualifiers, UnionBuilder, UnionType, binding_type, declaration_type, }; use crate::{Db, FxIndexSet, FxOrderSet, Program}; @@ -733,15 +732,6 @@ impl<'db> PlaceAndQualifiers<'db> { } } - pub(crate) fn materialize( - self, - db: &'db dyn Db, - materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, - ) -> PlaceAndQualifiers<'db> { - self.map_type(|ty| ty.materialize(db, materialization_kind, visitor)) - } - /// Transform place and qualifiers into a [`LookupResult`], /// a [`Result`] type in which the `Ok` variant represents a definitely defined place /// and the `Err` variant represents a place that is either definitely or possibly undefined. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cd45f0ca005c2..77b852046dae4 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2211,17 +2211,7 @@ impl<'db> Type<'db> { } Type::GenericAlias(alias) => { - let attr = Some(ClassType::from(*alias).class_member(db, name, policy)); - match alias.specialization(db).materialization_kind(db) { - None => attr, - Some(materialization_kind) => attr.map(|attr| { - attr.materialize( - db, - materialization_kind, - &ApplyTypeMappingVisitor::default(), - ) - }), - } + Some(ClassType::from(*alias).class_member(db, name, policy)) } Type::SubclassOf(subclass_of_ty) => { @@ -5172,19 +5162,17 @@ impl<'db> Type<'db> { db: &'db dyn Db, specialization: Specialization<'db>, ) -> Type<'db> { - let new_specialization = self.apply_type_mapping( - db, - &TypeMapping::ApplySpecialization(ApplySpecialization::Specialization(specialization)), - TypeContext::default(), - ); - match specialization.materialization_kind(db) { - None => new_specialization, - Some(materialization_kind) => new_specialization.materialize( - db, + let type_mapping = match specialization.materialization_kind(db) { + None => TypeMapping::ApplySpecialization(ApplySpecialization::Specialization( + specialization, + )), + Some(materialization_kind) => TypeMapping::ApplySpecializationWithMaterialization { + specialization: ApplySpecialization::Specialization(specialization), materialization_kind, - &ApplyTypeMappingVisitor::default(), - ), - } + }, + }; + + self.apply_type_mapping(db, &type_mapping, TypeContext::default()) } fn apply_type_mapping<'a>( @@ -5383,6 +5371,7 @@ impl<'db> Type<'db> { Type::ModuleLiteral(_) => match type_mapping { TypeMapping::ApplySpecialization(_) | + TypeMapping::ApplySpecializationWithMaterialization { .. } | TypeMapping::UniqueSpecialization { .. } | TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf(..) | @@ -5397,6 +5386,7 @@ impl<'db> Type<'db> { Type::LiteralValue(_) => match type_mapping { TypeMapping::ApplySpecialization(_) | + TypeMapping::ApplySpecializationWithMaterialization { .. } | TypeMapping::UniqueSpecialization { .. } | TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf { .. } | @@ -5411,6 +5401,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) => match type_mapping { TypeMapping::ApplySpecialization(_) | + TypeMapping::ApplySpecializationWithMaterialization { .. } | TypeMapping::UniqueSpecialization { .. } | TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf(..) | @@ -6175,6 +6166,13 @@ impl<'db> SelfBinding<'db> { pub enum TypeMapping<'a, 'db> { /// Applies a specialization to the type ApplySpecialization(ApplySpecialization<'a, 'db>), + /// Applies a specialization and materializes only substituted typevars. + /// + /// The `materialization_kind` is flipped in contravariant positions. + ApplySpecializationWithMaterialization { + specialization: ApplySpecialization<'a, 'db>, + materialization_kind: MaterializationKind, + }, /// Resets any specializations to contain unique synthetic type variables. UniqueSpecialization { // A list of synthetic type variables, and the types they replaced. @@ -6211,7 +6209,8 @@ impl<'db> TypeMapping<'_, 'db> { context: GenericContext<'db>, ) -> GenericContext<'db> { match self { - TypeMapping::ApplySpecialization(specialization) => { + TypeMapping::ApplySpecialization(specialization) + | TypeMapping::ApplySpecializationWithMaterialization { specialization, .. } => { // Filter out type variables that are already specialized // (i.e., mapped to a non-TypeVar type) GenericContext::from_typevar_instances( @@ -6267,6 +6266,13 @@ impl<'db> TypeMapping<'_, 'db> { TypeMapping::Materialize(materialization_kind) => { TypeMapping::Materialize(materialization_kind.flip()) } + TypeMapping::ApplySpecializationWithMaterialization { + specialization, + materialization_kind, + } => TypeMapping::ApplySpecializationWithMaterialization { + specialization: specialization.clone(), + materialization_kind: materialization_kind.flip(), + }, TypeMapping::PromoteLiterals(mode) => TypeMapping::PromoteLiterals(mode.flip()), TypeMapping::ApplySpecialization(_) | TypeMapping::UniqueSpecialization { .. } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 627c81826cb3a..bc74889549eff 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1330,17 +1330,7 @@ impl<'db> ClassType<'db> { let fallback_member_lookup = || { class_literal .own_class_member(db, inherited_generic_context, specialization, name) - .map_type(|ty| { - let ty = ty.apply_optional_specialization(db, specialization); - match specialization.map(|spec| spec.materialization_kind(db)) { - Some(Some(materialization_kind)) => ty.materialize( - db, - materialization_kind, - &ApplyTypeMappingVisitor::default(), - ), - _ => ty, - } - }) + .map_type(|ty| ty.apply_optional_specialization(db, specialization)) }; match name { diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index b73740880a6c6..41ed564beb454 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -283,6 +283,7 @@ impl<'db> KnownInstanceType<'db> { BoundTypeVarInstance::new(db, typevar, *binding_context, None), ), TypeMapping::ApplySpecialization(_) + | TypeMapping::ApplySpecializationWithMaterialization { .. } | TypeMapping::UniqueSpecialization { .. } | TypeMapping::PromoteLiterals(_) | TypeMapping::BindSelf(..) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 33a9cc662d9ef..3e0f0f2d1dc4f 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -217,7 +217,10 @@ impl<'db> CallableSignature<'db> { } } - if let TypeMapping::ApplySpecialization(specialization) = type_mapping { + if let TypeMapping::ApplySpecialization(specialization) + | TypeMapping::ApplySpecializationWithMaterialization { specialization, .. } = + type_mapping + { Self::from_overloads(self.overloads.iter().flat_map(|signature| { if let Some((prefix, paramspec)) = signature.parameters.find_paramspec_from_args_kwargs(db) diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index b86c6a9e8c8d3..c8e0e8fadcc4a 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -158,11 +158,15 @@ impl<'db> SubclassOfType<'db> { }, _ => Type::SubclassOf(self), }, - SubclassOfInner::TypeVar(typevar) => SubclassOfType::try_from_instance( - db, - typevar.apply_type_mapping_impl(db, type_mapping, visitor), - ) - .unwrap_or(SubclassOfType::subclass_of_unknown()), + SubclassOfInner::TypeVar(typevar) => { + let mapped = typevar.apply_type_mapping_impl(db, type_mapping, visitor); + if mapped.is_never() { + Type::Never + } else { + SubclassOfType::try_from_instance(db, mapped) + .unwrap_or(SubclassOfType::subclass_of_unknown()) + } + } } } diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 74b20481105df..7621e266968d0 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -12,10 +12,11 @@ use crate::{ semantic_index, }, types::{ - ApplyTypeMappingVisitor, CycleDetector, DynamicType, KnownClass, KnownInstanceType, - MaterializationKind, Parameter, Parameters, Type, TypeAliasType, TypeContext, TypeMapping, - TypeVarVariance, UnionBuilder, UnionType, any_over_type, binding_type, - definition_expression_type, tuple::Tuple, variance::VarianceInferable, visitor, + ApplySpecialization, ApplyTypeMappingVisitor, CycleDetector, DynamicType, KnownClass, + KnownInstanceType, MaterializationKind, Parameter, Parameters, Type, TypeAliasType, + TypeContext, TypeMapping, TypeVarVariance, UnionBuilder, UnionType, any_over_type, + binding_type, definition_expression_type, tuple::Tuple, variance::VarianceInferable, + visitor, }, }; @@ -844,26 +845,46 @@ impl<'db> BoundTypeVarInstance<'db> { type_mapping: &TypeMapping<'a, 'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Type<'db> { - match type_mapping { - TypeMapping::ApplySpecialization(specialization) => { + let mapped_specialization_type = + |specialization: &ApplySpecialization<'a, 'db>| -> Option> { let typevar = if self.is_paramspec(db) { self.without_paramspec_attr(db) } else { self }; - specialization - .get(db, typevar) - .map(|ty| { - if let Some(attr) = self.paramspec_attr(db) - && let Type::TypeVar(typevar) = ty - && typevar.is_paramspec(db) - { - return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); - } - ty - }) - .unwrap_or(Type::TypeVar(self)) + specialization.get(db, typevar).map(|ty| { + if let Some(attr) = self.paramspec_attr(db) + && let Type::TypeVar(typevar) = ty + && typevar.is_paramspec(db) + { + return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); + } + ty + }) + }; + + match type_mapping { + TypeMapping::ApplySpecialization(specialization) => { + mapped_specialization_type(specialization).unwrap_or(Type::TypeVar(self)) } + TypeMapping::ApplySpecializationWithMaterialization { + specialization, + materialization_kind, + } => mapped_specialization_type(specialization) + .map(|mapped| { + // Only materialize if the specialization actually substituted this + // typevar with a different type. A typevar that maps back to itself + // hasn't been substituted and should not be materialized. + if mapped == Type::TypeVar(self) { + mapped + } else { + // Materialization uses a different mapping mode. Reuse of the outer + // visitor can incorrectly hit a cache entry from specialization. + let materialization_visitor = ApplyTypeMappingVisitor::default(); + mapped.materialize(db, *materialization_kind, &materialization_visitor) + } + }) + .unwrap_or(Type::TypeVar(self)), TypeMapping::BindSelf(binding) => { if binding.should_bind(db, self) { binding.self_type() From 29cce180cf988dfed6f25f45461ce018a017e634 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Mar 2026 17:22:23 -0500 Subject: [PATCH 219/261] [ty] Support narrowing in ternary expressions (#23726) ## Summary Closes https://github.com/astral-sh/ty/issues/2804. --------- Co-authored-by: Alex Waygood --- .../resources/mdtest/narrow/truthiness.md | 51 +++++++++++++ crates/ty_python_semantic/src/types/narrow.rs | 75 +++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index e6ab0527cb778..0cf2f736cb8c2 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -331,6 +331,57 @@ def _(x: type[FalsyClass] | type[TruthyClass]): reveal_type(x and A()) # revealed: type[FalsyClass] | A ``` +## Narrowing with conditional expressions + +```py +def _(coinflip_1: bool, coinflip_2: bool): + if coinflip_1 if coinflip_2 else coinflip_1: + reveal_type(coinflip_1) # revealed: Literal[True] + else: + reveal_type(coinflip_1) # revealed: Literal[False] +``` + +## Conditional expressions with ambiguous branch constraints + +```py +from typing import Literal + +def _(flag: bool, x: Literal[0, 1], y: Literal[0, 1]): + if x if flag else y: + reveal_type(x) # revealed: Literal[0, 1] + reveal_type(y) # revealed: Literal[0, 1] + else: + reveal_type(x) # revealed: Literal[0, 1] + reveal_type(y) # revealed: Literal[0, 1] + +def _(flag: bool, x: Literal[0, 1, 2]): + if (x == 1) if flag else (x == 2): + reveal_type(x) # revealed: Literal[1, 2] + else: + reveal_type(x) # revealed: Literal[0, 2, 1] + +def _(flag: bool, x: Literal[0, 1], y: int): + if (x == 1) if flag else y: + reveal_type(x) # revealed: Literal[0, 1] +``` + +## Conditional expressions with statically known tests + +```py +from typing import Literal + +def _(x: Literal[0, 1], y: Literal[0, 1]): + if x if True else y: + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: Literal[0] + + if x if False else y: + reveal_type(y) # revealed: Literal[1] + else: + reveal_type(y) # revealed: Literal[0] +``` + ## Truthiness narrowing for `LiteralString` ```py diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 0163be2b1383e..73215a60d30e8 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -642,11 +642,77 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { self.evaluate_expression_node_predicate(&unary_op.operand, expression, !is_positive) } ast::Expr::BoolOp(bool_op) => self.evaluate_bool_op(bool_op, expression, is_positive), + ast::Expr::If(expr_if) => self.evaluate_expr_if(expr_if, expression, is_positive), ast::Expr::Named(expr_named) => self.evaluate_expr_named(expr_named, is_positive), _ => None, } } + fn merge_optional_constraints_and( + left: Option>, + right: Option>, + ) -> Option> { + match (left, right) { + (Some(mut left), Some(right)) => { + merge_constraints_and(&mut left, right); + Some(left) + } + (Some(left), None) => Some(left), + (None, Some(right)) => Some(right), + (None, None) => None, + } + } + + fn merge_optional_constraints_or( + left: Option>, + right: Option>, + ) -> Option> { + match (left, right) { + (Some(mut left), Some(right)) => { + merge_constraints_or(&mut left, right); + Some(left) + } + _ => None, + } + } + + fn evaluate_expr_if( + &mut self, + expr_if: &ast::ExprIf, + expression: Expression<'db>, + is_positive: bool, + ) -> Option> { + let test_truthiness = infer_expression_types(self.db, expression, TypeContext::default()) + .expression_type(&expr_if.test) + .bool(self.db); + + match test_truthiness { + Truthiness::AlwaysTrue => { + self.evaluate_expression_node_predicate(&expr_if.body, expression, is_positive) + } + Truthiness::AlwaysFalse => { + self.evaluate_expression_node_predicate(&expr_if.orelse, expression, is_positive) + } + Truthiness::Ambiguous => { + let body_constraints = Self::merge_optional_constraints_and( + self.evaluate_expression_node_predicate(&expr_if.test, expression, true), + self.evaluate_expression_node_predicate(&expr_if.body, expression, is_positive), + ); + let orelse_constraints = Self::merge_optional_constraints_and( + self.evaluate_expression_node_predicate(&expr_if.test, expression, false), + self.evaluate_expression_node_predicate( + &expr_if.orelse, + expression, + is_positive, + ), + ); + + // `a if c else b` is equivalent to `(c and a) or (not c and b)`. + Self::merge_optional_constraints_or(body_constraints, orelse_constraints) + } + } + } + fn evaluate_pattern_predicate_kind( &mut self, pattern_predicate_kind: &PatternPredicateKind<'db>, @@ -2186,6 +2252,8 @@ impl<'db, 'a> PossiblyNarrowedPlacesBuilder<'db, 'a> { } // Boolean operations combine places from all sub-expressions ast::Expr::BoolOp(bool_op) => self.expr_bool_op(bool_op), + // Conditional expressions combine places from all branches and the test. + ast::Expr::If(expr_if) => self.expr_if(expr_if), // Named expressions narrow both the target and the value ast::Expr::Named(expr_named) => { let mut places = self.simple_expr(&expr_named.target); @@ -2267,6 +2335,13 @@ impl<'db, 'a> PossiblyNarrowedPlacesBuilder<'db, 'a> { places } + fn expr_if(&self, expr_if: &ast::ExprIf) -> PossiblyNarrowedPlaces { + let mut places = self.expression_node(&expr_if.test); + places.extend(self.expression_node(&expr_if.body)); + places.extend(self.expression_node(&expr_if.orelse)); + places + } + /// Helper to add a potential narrowing target expression to the set. fn add_narrowing_target(&self, expr: &ast::Expr, places: &mut PossiblyNarrowedPlaces) { match expr { From 3880c50219d5eb1b1880d269c325d0b43e834ada Mon Sep 17 00:00:00 2001 From: sososonia-cyber Date: Fri, 6 Mar 2026 07:32:31 +0800 Subject: [PATCH 220/261] docs: Fix misleading description for B904 rule (#23731) Co-authored-by: Atlas Bot Co-authored-by: Amethyst Reese --- .../rules/raise_without_from_inside_except.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs index 41bc2ebdf8fdd..b918074aea546 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs @@ -18,9 +18,11 @@ use crate::checkers::ast::Checker; /// printing the stack trace, chained exceptions are displayed in such a way /// so as make it easier to trace the exception back to its root cause. /// -/// When raising an exception from within an `except` clause, always include a -/// `from` clause to facilitate exception chaining. If the exception is not -/// chained, it will be difficult to trace the exception back to its root cause. +/// When raising a new exception from within an `except` clause, it's recommended to +/// include a `from` clause to explicitly set the exception's cause. Without it, +/// Python will implicitly chain from the current exception (setting `__context__`), +/// but the `__cause__` attribute won't be set, which may make debugging slightly +/// more difficult. /// /// ## Example /// ```python From 7d8c6422b1538b3dc44c8ecd4e1c883c76fdea76 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 5 Mar 2026 15:40:48 -0800 Subject: [PATCH 221/261] [ty] do not union Unknown into unannotated container types (#23718) ## Summary Part of https://github.com/astral-sh/ty/issues/1240 Stop unioning `Unknown` into the types of un-annotated container literals. We discussed perhaps continuing to union `Unknown` if the inferred type is a singleton type like `None`. I'd like to explore this as a separate change so we can see the ecosystem impact more clearly. ## Test Plan Adjusted many mdtest expectations. There's one test case that regresses with this change, because we don't fully support union type contexts (it can require a lot of repeat inference in pathological cases). So `x10: list[int | str] | list[int | None] = [1, 2, 3]` previously passed only because we inferred the RHS as `list[Unknown | int]` -- now we infer it as `list[int]` and the assignment fails due to invariance. I've kept this test as a TODO since it's not trivial to fix. Mypy errors in the same way we now do, suggesting it's not necessarily a huge priority either. ## Ecosystem This change is expected to cause new diagnostics and some false positives, since we are replacing very-forgiving gradual types with non-gradual inference heuristics. Many of these issues could be solved or significantly mitigated by https://github.com/astral-sh/ty/issues/1473, depending how far we are able to go with that, and particularly whether we can afford to apply it also to container literals which are not empty at construction. The downside of broad application of this approach is that in some cases it could cause us to widen container types when the user actually just made a mistake and added the wrong thing to a container, and would prefer an error at that location. Some categories of new error that show up in the ecosystem report: ### Implicit TypedDicts These are cases where the dictionary is heterogeneous and would ideally be typed as a `TypedDict` but isn't, for example: ```py def make_person(photo: bytes | None): person = {"name": "Pat", age: 29} if photo is not None: person["photo"] = photo ``` We (and pyrefly, and pyright in strict mode) error on the last line here because we already inferred `dict[str, str | int]`, so we can't add a `bytes` value. Mypy prefers common-base joins over union joins, so it infers `dict[str, object]`, which avoids the error adding a `bytes` value. This means the value type is less precise, which theoretically means potentially more errors using values from the dict later. But in practice with this heterogeneous pattern, either `object` or the union will cause similar problems when using values from the dict -- in either case you'd probably have to cast or narrow. Pyright (in non-strict mode) has a special case where it falls back to `Unknown` when it sees heterogenous value types, so it infers this as `dict[str, Unknown]`. I think we could consider either the mypy or pyright approaches here, but we don't need to do it in this PR; we can file an issue and consider it as a follow-up. Another symptom of this same root cause is repetitive diagnostics arising from a large union inferred as value type; the same fixes would address this. ### Negative intersections, particularly with e.g. `~AlwaysFalsy` or `~None`. Example: ```py class A: ... def _(a: A | None) -> dict[str, A]: if a: d = {"a": a} return d return {} ``` We error on `return d` because "expected `dict[str, A]`, found `dict[str, A & ~AlwaysFalsy]`". This is an issue specific to intersection types, so no other type checker has this problem. I think when we "promote literals" (we may need to give this operation a broader name -- it's really "type promotion to give a better inferred type when invariance means too-precise is bad") we should also eliminate all negative types from intersections. I would prefer to do this as a separate PR for easier review and better visibility of ecosystem impact, but I think it's high priority to land soon after this PR (ideally before a release). ### Overly-precise inference for singleton `None` This did show up, to the tune of ~100 new diagnostics ([example](https://github.com/pytorch/ignite/blob/b73a4c20e991b3e14949f2a69651ed2a7219f2fd/tests/ignite/engine/test_engine.py#L158)), so I think it is worth addressing as a follow-up. --- crates/ruff_benchmark/benches/ty_walltime.rs | 6 +- crates/ty_ide/src/hover.rs | 8 +- crates/ty_ide/src/inlay_hints.rs | 1147 ++++++----------- .../mdtest/assignment/annotations.md | 22 +- .../resources/mdtest/attributes.md | 8 +- .../resources/mdtest/bidirectional.md | 2 +- .../resources/mdtest/call/type.md | 2 +- .../resources/mdtest/call/union.md | 6 +- .../resources/mdtest/comprehensions/basic.md | 29 +- .../resources/mdtest/cycle.md | 2 +- .../resources/mdtest/del.md | 4 +- .../mdtest/generics/pep695/aliases.md | 4 +- .../mdtest/generics/pep695/paramspec.md | 2 +- .../resources/mdtest/import/dunder_all.md | 2 +- .../mdtest/literal/collections/dictionary.md | 26 +- .../mdtest/literal/collections/list.md | 10 +- .../mdtest/literal/collections/set.md | 10 +- .../resources/mdtest/literal_promotion.md | 38 +- .../resources/mdtest/subscript/lists.md | 6 +- .../resources/mdtest/type_compendium/tuple.md | 4 +- .../resources/mdtest/unpacking.md | 4 +- .../src/types/infer/builder.rs | 16 +- scripts/check_ecosystem.py | 2 +- .../src/benchmark/test_lsp_diagnostics.py | 8 +- 24 files changed, 493 insertions(+), 875 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index d367a3020e9fe..42b245e31cb5d 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -128,7 +128,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY310, }, - 400, + 350, ); static FREQTRADE: Benchmark = Benchmark::new( @@ -171,7 +171,7 @@ static PANDAS: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 4500, + 4600, ); static PYDANTIC: Benchmark = Benchmark::new( @@ -215,7 +215,7 @@ static TANJUN: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 150, + 120, ); static STATIC_FRAME: Benchmark = Benchmark::new( diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 3feb907dd9aca..1c2db06ca3e86 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -4547,11 +4547,11 @@ def function(): "#, ); - assert_snapshot!(test.hover(), @" - list[Unknown | int] + assert_snapshot!(test.hover(), @r###" + list[int] --------------------------------------------- ```python - list[Unknown | int] + list[int] ``` --------------------------------------------- info[hover]: Hovered content is @@ -4562,7 +4562,7 @@ def function(): | | | source | - "); + "###); let test = cursor_test( r#" diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 9701abbccac25..87e40c7537a20 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -2664,19 +2664,19 @@ mod tests { "#, ); - assert_snapshot!(test.inlay_hints(), @r#" - - a[: list[Unknown | int]] = [1, 2] - b[: list[Unknown | int | float]] = [1.0, 2.0] - c[: list[Unknown | bool]] = [True, False] - d[: list[Unknown | None]] = [None, None] - e[: list[Unknown | str]] = ["hel", "lo"] - f[: list[Unknown | str]] = ['the', 're'] - g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - j[: list[Unknown | int | float]] = [+1, +2.0] - k[: list[Unknown | int | float]] = [-1, -2.0] + assert_snapshot!(test.inlay_hints(), @r###" + + a[: list[int]] = [1, 2] + b[: list[int | float]] = [1.0, 2.0] + c[: list[bool]] = [True, False] + d[: list[None]] = [None, None] + e[: list[str]] = ["hel", "lo"] + f[: list[str]] = ['the', 're'] + g[: list[str]] = [f"{ft}", f"{ft}"] + h[: list[Template]] = [t"wow %d", t"wow %d"] + i[: list[bytes]] = [b'/x01', b'/x02'] + j[: list[int | float]] = [+1, +2.0] + k[: list[int | float]] = [-1, -2.0] --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target @@ -2690,28 +2690,10 @@ mod tests { info: Source --> main2.py:2:5 | - 2 | a[: list[Unknown | int]] = [1, 2] + 2 | a[: list[int]] = [1, 2] | ^^^^ - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:2:10 - | - 2 | a[: list[Unknown | int]] = [1, 2] - | ^^^^^^^ - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] + 3 | b[: list[int | float]] = [1.0, 2.0] + 4 | c[: list[bool]] = [True, False] | info[inlay-hint-location]: Inlay Hint Target @@ -2724,12 +2706,12 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:2:20 + --> main2.py:2:10 | - 2 | a[: list[Unknown | int]] = [1, 2] - | ^^^ - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] + 2 | a[: list[int]] = [1, 2] + | ^^^ + 3 | b[: list[int | float]] = [1.0, 2.0] + 4 | c[: list[bool]] = [True, False] | info[inlay-hint-location]: Inlay Hint Target @@ -2743,30 +2725,11 @@ mod tests { info: Source --> main2.py:3:5 | - 2 | a[: list[Unknown | int]] = [1, 2] - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] + 2 | a[: list[int]] = [1, 2] + 3 | b[: list[int | float]] = [1.0, 2.0] | ^^^^ - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:3:10 - | - 2 | a[: list[Unknown | int]] = [1, 2] - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - | ^^^^^^^ - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] + 4 | c[: list[bool]] = [True, False] + 5 | d[: list[None]] = [None, None] | info[inlay-hint-location]: Inlay Hint Target @@ -2779,13 +2742,13 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:3:20 + --> main2.py:3:10 | - 2 | a[: list[Unknown | int]] = [1, 2] - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - | ^^^ - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] + 2 | a[: list[int]] = [1, 2] + 3 | b[: list[int | float]] = [1.0, 2.0] + | ^^^ + 4 | c[: list[bool]] = [True, False] + 5 | d[: list[None]] = [None, None] | info[inlay-hint-location]: Inlay Hint Target @@ -2797,13 +2760,13 @@ mod tests { 662 | """Convert a string or number to a floating-point number, if possible.""" | info: Source - --> main2.py:3:26 + --> main2.py:3:16 | - 2 | a[: list[Unknown | int]] = [1, 2] - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - | ^^^^^ - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] + 2 | a[: list[int]] = [1, 2] + 3 | b[: list[int | float]] = [1.0, 2.0] + | ^^^^^ + 4 | c[: list[bool]] = [True, False] + 5 | d[: list[None]] = [None, None] | info[inlay-hint-location]: Inlay Hint Target @@ -2817,32 +2780,12 @@ mod tests { info: Source --> main2.py:4:5 | - 2 | a[: list[Unknown | int]] = [1, 2] - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] + 2 | a[: list[int]] = [1, 2] + 3 | b[: list[int | float]] = [1.0, 2.0] + 4 | c[: list[bool]] = [True, False] | ^^^^ - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:4:10 - | - 2 | a[: list[Unknown | int]] = [1, 2] - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] - | ^^^^^^^ - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] + 5 | d[: list[None]] = [None, None] + 6 | e[: list[str]] = ["hel", "lo"] | info[inlay-hint-location]: Inlay Hint Target @@ -2855,14 +2798,14 @@ mod tests { 2620 | The builtins True and False are the only two instances of the class bool. | info: Source - --> main2.py:4:20 + --> main2.py:4:10 | - 2 | a[: list[Unknown | int]] = [1, 2] - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] - | ^^^^ - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] + 2 | a[: list[int]] = [1, 2] + 3 | b[: list[int | float]] = [1.0, 2.0] + 4 | c[: list[bool]] = [True, False] + | ^^^^ + 5 | d[: list[None]] = [None, None] + 6 | e[: list[str]] = ["hel", "lo"] | info[inlay-hint-location]: Inlay Hint Target @@ -2876,32 +2819,12 @@ mod tests { info: Source --> main2.py:5:5 | - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] + 3 | b[: list[int | float]] = [1.0, 2.0] + 4 | c[: list[bool]] = [True, False] + 5 | d[: list[None]] = [None, None] | ^^^^ - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:5:10 - | - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] - | ^^^^^^^ - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] + 6 | e[: list[str]] = ["hel", "lo"] + 7 | f[: list[str]] = ['the', 're'] | info[inlay-hint-location]: Inlay Hint Target @@ -2914,14 +2837,14 @@ mod tests { 970 | """The type of the None singleton.""" | info: Source - --> main2.py:5:20 + --> main2.py:5:10 | - 3 | b[: list[Unknown | int | float]] = [1.0, 2.0] - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] - | ^^^^ - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] + 3 | b[: list[int | float]] = [1.0, 2.0] + 4 | c[: list[bool]] = [True, False] + 5 | d[: list[None]] = [None, None] + | ^^^^ + 6 | e[: list[str]] = ["hel", "lo"] + 7 | f[: list[str]] = ['the', 're'] | info[inlay-hint-location]: Inlay Hint Target @@ -2935,32 +2858,12 @@ mod tests { info: Source --> main2.py:6:5 | - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] + 4 | c[: list[bool]] = [True, False] + 5 | d[: list[None]] = [None, None] + 6 | e[: list[str]] = ["hel", "lo"] | ^^^^ - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:6:10 - | - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - | ^^^^^^^ - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] + 7 | f[: list[str]] = ['the', 're'] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] | info[inlay-hint-location]: Inlay Hint Target @@ -2973,14 +2876,14 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:6:20 + --> main2.py:6:10 | - 4 | c[: list[Unknown | bool]] = [True, False] - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - | ^^^ - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] + 4 | c[: list[bool]] = [True, False] + 5 | d[: list[None]] = [None, None] + 6 | e[: list[str]] = ["hel", "lo"] + | ^^^ + 7 | f[: list[str]] = ['the', 're'] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] | info[inlay-hint-location]: Inlay Hint Target @@ -2994,32 +2897,12 @@ mod tests { info: Source --> main2.py:7:5 | - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] + 5 | d[: list[None]] = [None, None] + 6 | e[: list[str]] = ["hel", "lo"] + 7 | f[: list[str]] = ['the', 're'] | ^^^^ - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:7:10 - | - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] - | ^^^^^^^ - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] | info[inlay-hint-location]: Inlay Hint Target @@ -3032,14 +2915,14 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:7:20 + --> main2.py:7:10 | - 5 | d[: list[Unknown | None]] = [None, None] - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] - | ^^^ - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] + 5 | d[: list[None]] = [None, None] + 6 | e[: list[str]] = ["hel", "lo"] + 7 | f[: list[str]] = ['the', 're'] + | ^^^ + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] | info[inlay-hint-location]: Inlay Hint Target @@ -3053,32 +2936,12 @@ mod tests { info: Source --> main2.py:8:5 | - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] + 6 | e[: list[str]] = ["hel", "lo"] + 7 | f[: list[str]] = ['the', 're'] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] | ^^^^ - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:8:10 - | - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - | ^^^^^^^ - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] | info[inlay-hint-location]: Inlay Hint Target @@ -3091,14 +2954,14 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:8:20 + --> main2.py:8:10 | - 6 | e[: list[Unknown | str]] = ["hel", "lo"] - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - | ^^^ - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] + 6 | e[: list[str]] = ["hel", "lo"] + 7 | f[: list[str]] = ['the', 're'] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] + | ^^^ + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] | info[inlay-hint-location]: Inlay Hint Target @@ -3112,32 +2975,12 @@ mod tests { info: Source --> main2.py:9:5 | - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] + 7 | f[: list[str]] = ['the', 're'] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] | ^^^^ - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:9:10 - | - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - | ^^^^^^^ - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + 11 | j[: list[int | float]] = [+1, +2.0] | info[inlay-hint-location]: Inlay Hint Target @@ -3149,14 +2992,14 @@ mod tests { 11 | """Template object""" | info: Source - --> main2.py:9:20 + --> main2.py:9:10 | - 7 | f[: list[Unknown | str]] = ['the', 're'] - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - | ^^^^^^^^ - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] + 7 | f[: list[str]] = ['the', 're'] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] + | ^^^^^^^^ + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + 11 | j[: list[int | float]] = [+1, +2.0] | info[inlay-hint-location]: Inlay Hint Target @@ -3170,32 +3013,12 @@ mod tests { info: Source --> main2.py:10:5 | - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] | ^^^^ - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:10:10 - | - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - | ^^^^^^^ - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] + 11 | j[: list[int | float]] = [+1, +2.0] + 12 | k[: list[int | float]] = [-1, -2.0] | info[inlay-hint-location]: Inlay Hint Target @@ -3208,14 +3031,14 @@ mod tests { 1450 | bytes(string, encoding[, errors]) -> bytes | info: Source - --> main2.py:10:20 + --> main2.py:10:10 | - 8 | g[: list[Unknown | str]] = [f"{ft}", f"{ft}"] - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - | ^^^^^ - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] + 8 | g[: list[str]] = [f"{ft}", f"{ft}"] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + | ^^^^^ + 11 | j[: list[int | float]] = [+1, +2.0] + 12 | k[: list[int | float]] = [-1, -2.0] | info[inlay-hint-location]: Inlay Hint Target @@ -3229,30 +3052,11 @@ mod tests { info: Source --> main2.py:11:5 | - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + 11 | j[: list[int | float]] = [+1, +2.0] | ^^^^ - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:11:10 - | - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - | ^^^^^^^ - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] + 12 | k[: list[int | float]] = [-1, -2.0] | info[inlay-hint-location]: Inlay Hint Target @@ -3265,13 +3069,13 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:11:20 + --> main2.py:11:10 | - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - | ^^^ - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + 11 | j[: list[int | float]] = [+1, +2.0] + | ^^^ + 12 | k[: list[int | float]] = [-1, -2.0] | info[inlay-hint-location]: Inlay Hint Target @@ -3283,13 +3087,13 @@ mod tests { 662 | """Convert a string or number to a floating-point number, if possible.""" | info: Source - --> main2.py:11:26 + --> main2.py:11:16 | - 9 | h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"] - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - | ^^^^^ - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] + 9 | h[: list[Template]] = [t"wow %d", t"wow %d"] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + 11 | j[: list[int | float]] = [+1, +2.0] + | ^^^^^ + 12 | k[: list[int | float]] = [-1, -2.0] | info[inlay-hint-location]: Inlay Hint Target @@ -3303,30 +3107,12 @@ mod tests { info: Source --> main2.py:12:5 | - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + 11 | j[: list[int | float]] = [+1, +2.0] + 12 | k[: list[int | float]] = [-1, -2.0] | ^^^^ | - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:12:10 - | - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] - | ^^^^^^^ - | - info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:348:7 | @@ -3337,12 +3123,12 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:12:20 + --> main2.py:12:10 | - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] - | ^^^ + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + 11 | j[: list[int | float]] = [+1, +2.0] + 12 | k[: list[int | float]] = [-1, -2.0] + | ^^^ | info[inlay-hint-location]: Inlay Hint Target @@ -3354,32 +3140,31 @@ mod tests { 662 | """Convert a string or number to a floating-point number, if possible.""" | info: Source - --> main2.py:12:26 + --> main2.py:12:16 | - 10 | i[: list[Unknown | bytes]] = [b'/x01', b'/x02'] - 11 | j[: list[Unknown | int | float]] = [+1, +2.0] - 12 | k[: list[Unknown | int | float]] = [-1, -2.0] - | ^^^^^ + 10 | i[: list[bytes]] = [b'/x01', b'/x02'] + 11 | j[: list[int | float]] = [+1, +2.0] + 12 | k[: list[int | float]] = [-1, -2.0] + | ^^^^^ | --------------------------------------------- info[inlay-hint-edit]: File after edits info: Source - from ty_extensions import Unknown from string.templatelib import Template - a: list[Unknown | int] = [1, 2] - b: list[Unknown | int | float] = [1.0, 2.0] - c: list[Unknown | bool] = [True, False] - d: list[Unknown | None] = [None, None] - e: list[Unknown | str] = ["hel", "lo"] - f: list[Unknown | str] = ['the', 're'] - g: list[Unknown | str] = [f"{ft}", f"{ft}"] - h: list[Unknown | Template] = [t"wow %d", t"wow %d"] - i: list[Unknown | bytes] = [b'/x01', b'/x02'] - j: list[Unknown | int | float] = [+1, +2.0] - k: list[Unknown | int | float] = [-1, -2.0] - "#); + a: list[int] = [1, 2] + b: list[int | float] = [1.0, 2.0] + c: list[bool] = [True, False] + d: list[None] = [None, None] + e: list[str] = ["hel", "lo"] + f: list[str] = ['the', 're'] + g: list[str] = [f"{ft}", f"{ft}"] + h: list[Template] = [t"wow %d", t"wow %d"] + i: list[bytes] = [b'/x01', b'/x02'] + j: list[int | float] = [+1, +2.0] + k: list[int | float] = [-1, -2.0] + "###); } #[test] @@ -3564,17 +3349,17 @@ mod tests { "#, ); - assert_snapshot!(test.inlay_hints(), @r#" + assert_snapshot!(test.inlay_hints(), @r###" class MyClass[T, U]: def __init__(self, x: list[T], y: tuple[U, U]): self.x[: list[T@MyClass]] = x self.y[: tuple[U@MyClass, U@MyClass]] = y - x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) - a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) - c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target @@ -3611,7 +3396,7 @@ mod tests { 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y | ^^^^^ 6 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) | info[inlay-hint-location]: Inlay Hint Target @@ -3627,30 +3412,10 @@ mod tests { | 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y 6 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) | ^^^^^^^ - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:7:13 - | - 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y - 6 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - | ^^^^^^^ - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | info[inlay-hint-location]: Inlay Hint Target @@ -3663,14 +3428,14 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:7:23 + --> main2.py:7:13 | 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y 6 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - | ^^^ - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + | ^^^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | info[inlay-hint-location]: Inlay Hint Target @@ -3683,14 +3448,14 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:7:28 + --> main2.py:7:18 | 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y 6 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - | ^^^ - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + | ^^^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | info[inlay-hint-location]: Inlay Hint Target @@ -3703,14 +3468,14 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:7:45 + --> main2.py:7:35 | 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y 6 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - | ^ - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + | ^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | info[inlay-hint-location]: Inlay Hint Target @@ -3723,14 +3488,14 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:7:55 + --> main2.py:7:45 | 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y 6 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - | ^ - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + | ^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | info[inlay-hint-location]: Inlay Hint Target @@ -3744,11 +3509,11 @@ mod tests { info: Source --> main2.py:8:5 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3762,30 +3527,11 @@ mod tests { info: Source --> main2.py:8:11 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^^^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:8:19 - | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^^^^^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3798,13 +3544,13 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:8:29 + --> main2.py:8:19 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3817,13 +3563,13 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:8:34 + --> main2.py:8:24 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3835,32 +3581,13 @@ mod tests { 4 | self.x = x | info: Source - --> main2.py:8:40 - | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^^^^^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:8:48 + --> main2.py:8:30 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^^^^^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^^^^^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3873,13 +3600,13 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:8:58 + --> main2.py:8:38 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3892,13 +3619,13 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:8:63 + --> main2.py:8:43 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^^^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3911,13 +3638,13 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:8:82 + --> main2.py:8:62 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3930,13 +3657,13 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:8:92 + --> main2.py:8:72 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3949,13 +3676,13 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:8:117 + --> main2.py:8:97 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3968,13 +3695,13 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:8:127 + --> main2.py:8:107 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - | ^ - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^ + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -3988,30 +3715,11 @@ mod tests { info: Source --> main2.py:9:5 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^^^^^^^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:9:13 - | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^^^^^^^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4024,13 +3732,13 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:9:23 + --> main2.py:9:13 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^^^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^^^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4043,13 +3751,13 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:9:28 + --> main2.py:9:18 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^^^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^^^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4061,32 +3769,13 @@ mod tests { 4 | self.x = x | info: Source - --> main2.py:9:39 - | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^^^^^^^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + --> main2.py:9:29 | - info: Source - --> main2.py:9:47 - | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^^^^^^^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^^^^^^^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4099,13 +3788,13 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:9:57 + --> main2.py:9:37 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^^^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^^^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4118,13 +3807,13 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:9:62 + --> main2.py:9:42 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^^^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^^^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4137,13 +3826,13 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:9:79 + --> main2.py:9:59 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4156,13 +3845,13 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:9:89 + --> main2.py:9:69 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4175,13 +3864,13 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:9:114 + --> main2.py:9:94 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4194,13 +3883,13 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:9:124 + --> main2.py:9:104 | - 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - | ^ - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + | ^ + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | info[inlay-hint-location]: Inlay Hint Target @@ -4214,30 +3903,12 @@ mod tests { info: Source --> main2.py:10:5 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^^^^^ | - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:10:13 - | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^^^^^^^ - | - info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:348:7 | @@ -4248,12 +3919,12 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:10:23 + --> main2.py:10:13 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^^^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^ | info[inlay-hint-location]: Inlay Hint Target @@ -4266,12 +3937,12 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:10:28 + --> main2.py:10:18 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^^^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^ | info[inlay-hint-location]: Inlay Hint Target @@ -4283,30 +3954,12 @@ mod tests { 4 | self.x = x | info: Source - --> main2.py:10:39 - | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^^^^^^^ - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:10:47 + --> main2.py:10:29 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^^^^^^^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^^^^^ | info[inlay-hint-location]: Inlay Hint Target @@ -4319,12 +3972,12 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:10:57 + --> main2.py:10:37 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^^^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^ | info[inlay-hint-location]: Inlay Hint Target @@ -4337,12 +3990,12 @@ mod tests { 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str | info: Source - --> main2.py:10:62 + --> main2.py:10:42 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^^^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^^^ | info[inlay-hint-location]: Inlay Hint Target @@ -4355,12 +4008,12 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:10:80 + --> main2.py:10:60 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^ | info[inlay-hint-location]: Inlay Hint Target @@ -4373,12 +4026,12 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:10:90 + --> main2.py:10:70 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^ | info[inlay-hint-location]: Inlay Hint Target @@ -4391,12 +4044,12 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:10:115 + --> main2.py:10:95 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^ | info[inlay-hint-location]: Inlay Hint Target @@ -4409,29 +4062,28 @@ mod tests { 5 | self.y = y | info: Source - --> main2.py:10:125 + --> main2.py:10:105 | - 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… - 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… - 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… - | ^ + 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) + 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + | ^ | --------------------------------------------- info[inlay-hint-edit]: File after edits info: Source - from ty_extensions import Unknown class MyClass[T, U]: def __init__(self, x: list[T], y: tuple[U, U]): self.x = x self.y = y - x: MyClass[Unknown | int, str] = MyClass([42], ("a", "b")) - y: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]] = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + x: MyClass[int, str] = MyClass([42], ("a", "b")) + y: tuple[MyClass[int, str], MyClass[int, str]] = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) - "#); + "###); } #[test] @@ -4735,11 +4387,11 @@ mod tests { foo(y[0])", ); - assert_snapshot!(test.inlay_hints(), @r#" + assert_snapshot!(test.inlay_hints(), @r###" def foo(x: int): pass - x[: list[Unknown | int]] = [1] - y[: list[Unknown | int]] = [2] + x[: list[int]] = [1] + y[: list[int]] = [2] foo(x[0]) foo([x=]y[0]) @@ -4756,27 +4408,9 @@ mod tests { --> main2.py:3:5 | 2 | def foo(x: int): pass - 3 | x[: list[Unknown | int]] = [1] + 3 | x[: list[int]] = [1] | ^^^^ - 4 | y[: list[Unknown | int]] = [2] - | - - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:3:10 - | - 2 | def foo(x: int): pass - 3 | x[: list[Unknown | int]] = [1] - | ^^^^^^^ - 4 | y[: list[Unknown | int]] = [2] + 4 | y[: list[int]] = [2] | info[inlay-hint-location]: Inlay Hint Target @@ -4789,12 +4423,12 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:3:20 + --> main2.py:3:10 | 2 | def foo(x: int): pass - 3 | x[: list[Unknown | int]] = [1] - | ^^^ - 4 | y[: list[Unknown | int]] = [2] + 3 | x[: list[int]] = [1] + | ^^^ + 4 | y[: list[int]] = [2] | info[inlay-hint-location]: Inlay Hint Target @@ -4809,33 +4443,13 @@ mod tests { --> main2.py:4:5 | 2 | def foo(x: int): pass - 3 | x[: list[Unknown | int]] = [1] - 4 | y[: list[Unknown | int]] = [2] + 3 | x[: list[int]] = [1] + 4 | y[: list[int]] = [2] | ^^^^ 5 | 6 | foo(x[0]) | - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions.pyi:14:1 - | - 13 | # Types - 14 | Unknown = object() - | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() - | - info: Source - --> main2.py:4:10 - | - 2 | def foo(x: int): pass - 3 | x[: list[Unknown | int]] = [1] - 4 | y[: list[Unknown | int]] = [2] - | ^^^^^^^ - 5 | - 6 | foo(x[0]) - | - info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:348:7 | @@ -4846,12 +4460,12 @@ mod tests { 350 | int(x, base=10) -> integer | info: Source - --> main2.py:4:20 + --> main2.py:4:10 | 2 | def foo(x: int): pass - 3 | x[: list[Unknown | int]] = [1] - 4 | y[: list[Unknown | int]] = [2] - | ^^^ + 3 | x[: list[int]] = [1] + 4 | y[: list[int]] = [2] + | ^^^ 5 | 6 | foo(x[0]) | @@ -4875,15 +4489,14 @@ mod tests { --------------------------------------------- info[inlay-hint-edit]: File after edits info: Source - from ty_extensions import Unknown def foo(x: int): pass - x: list[Unknown | int] = [1] - y: list[Unknown | int] = [2] + x: list[int] = [1] + y: list[int] = [2] foo(x[0]) foo(y[0]) - "#); + "###); } #[test] diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index 9772789ecc10e..1b464fd1a2f4c 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -445,13 +445,14 @@ reveal_type(x8) # revealed: Literal[True] x9: int | str = f2(True) reveal_type(x9) # revealed: Literal[True] -# TODO: We could choose a concrete type here. +# TODO: Should not error. We could choose a concrete type here (pyright arbitrarily picks the +# first), or keep the union (pyrefly does this). Mypy infers `list[int]` and errors. +# error: [invalid-assignment] x10: list[int | str] | list[int | None] = [1, 2, 3] -reveal_type(x10) # revealed: list[Unknown | int] +reveal_type(x10) # revealed: list[int | str] | list[int | None] -# TODO: And here similarly. x11: Sequence[int | str] | Sequence[int | None] = [1, 2, 3] -reveal_type(x11) # revealed: list[Unknown | int] +reveal_type(x11) # revealed: list[int] ``` ## Annotations influence generic call argument inference @@ -480,12 +481,12 @@ reveal_type(x2) # revealed: TD # error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor" # error: [invalid-key] "Unknown key "y" for TypedDict `TD`" -# error: [invalid-assignment] "Object of type `TD | dict[Unknown | str, Unknown | int]` is not assignable to `TD`" +# error: [invalid-assignment] "Object of type `TD | dict[str, int]` is not assignable to `TD`" x3: TD = first([{"y": 0}, {"x": 1}]) # error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor" # error: [invalid-key] "Unknown key "y" for TypedDict `TD`" -# error: [invalid-assignment] "Object of type `TD | None | dict[Unknown | str, Unknown | int]` is not assignable to `TD | None`" +# error: [invalid-assignment] "Object of type `TD | None | dict[str, int]` is not assignable to `TD | None`" x4: TD | None = first([{"y": 0}, {"x": 1}]) ``` @@ -708,19 +709,16 @@ x6: Iterable[list[Any]] = [[1, 2, 3]] reveal_type(x6) # revealed: list[list[Any]] x7: Sequence[Any] = [i for i in [1, 2, 3]] -# TODO: This should infer `list[int]`. -reveal_type(x7) # revealed: list[Unknown | int] +reveal_type(x7) # revealed: list[int] x8: MutableSequence[Any] = [i for i in [1, 2, 3]] reveal_type(x8) # revealed: list[Any] x9: Iterable[Any] = [i for i in [1, 2, 3]] -# TODO: This should infer `list[int]`. -reveal_type(x9) # revealed: list[Unknown | int] +reveal_type(x9) # revealed: list[int] x10: Iterable[Iterable[Any]] = [[i] for i in [1, 2, 3]] -# TODO: This should infer `list[list[int]]`. -reveal_type(x10) # revealed: list[list[Unknown | int]] +reveal_type(x10) # revealed: list[list[int]] x11: list[Iterable[Any]] = [[i] for i in [1, 2, 3]] reveal_type(x11) # revealed: list[Iterable[Any]] diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index e11d0e39f79b7..363bb2a8c938d 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2643,8 +2643,8 @@ class C3: def replace_with(self, other: "C3"): self.x = [self.x[0].flip()] -# TODO: should be `Unknown | list[Unknown | Sub] | list[Unknown | Base]` -reveal_type(C3(Sub()).x) # revealed: Unknown | list[Unknown | Sub] | list[Divergent] +# TODO: should be `Unknown | list[Sub] | list[Base]` +reveal_type(C3(Sub()).x) # revealed: Unknown | list[Sub] | list[Divergent] ``` And cycles between many attributes: @@ -2702,8 +2702,8 @@ class ManyCycles2: self.x3 = [1] def f1(self: "ManyCycles2"): - # TODO: should be Unknown | list[Unknown | int] | list[Divergent] - reveal_type(self.x3) # revealed: Unknown | list[Unknown | int] | list[Unknown] | list[Divergent] + # TODO: should be Unknown | list[int] | list[Divergent] + reveal_type(self.x3) # revealed: Unknown | list[int] | list[Unknown] | list[Divergent] self.x1 = [self.x2] + [self.x3] self.x2 = [self.x1] + [self.x3] diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 602ba79d6b1fb..c4ffe8316e363 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -64,7 +64,7 @@ d3: dict[str, int] = {"x": 1} d4: TD = dict(x=1) d5: TD = dict(x="1") # error: [invalid-argument-type] -reveal_type(d1) # revealed: dict[Unknown | str, Unknown | int] +reveal_type(d1) # revealed: dict[str, int] reveal_type(d2) # revealed: TD reveal_type(d3) # revealed: dict[str, int] reveal_type(d4) # revealed: TD diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index eea1658a4e740..4a1fc9bc2833b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -541,7 +541,7 @@ type("Foo", Base, {}) # error: 17 [invalid-base] "Invalid class base with type `Literal[2]`" type("Foo", (1, 2), {}) -# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`" +# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[bytes, int]`" type("Foo", (Base,), {b"attr": 1}) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index ddb56cb51a38b..3a3a50e6da802 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -853,12 +853,12 @@ Type inference accounts for parameter type annotations across all signatures in ```py from typing import TypedDict, overload -class T(TypedDict): +class TD(TypedDict): x: int def _(flag: bool): if flag: - def f(x: T) -> int: + def f(x: TD) -> int: return 1 else: @@ -867,7 +867,7 @@ def _(flag: bool): x = f({"x": 1}) reveal_type(x) # revealed: int - # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int] & dict[Unknown | str, Unknown | int]`" + # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `TD`, found `dict[str, int]`" f({"y": 1}) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index ceecb1c7d515b..6997b8e84f601 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -129,7 +129,7 @@ The type of the expression being iterated over is immutable, and so should not b ```py # TODO: This should reveal `Literal["a", "b"]` -# revealed: Unknown | str +# revealed: str x = [reveal_type(string) for string in ["a", "b"]] ``` @@ -140,16 +140,16 @@ The type of the comprehension expression itself should reflect the inferred elem ```py from typing import TypedDict, Literal -# revealed: list[Unknown | int] +# revealed: list[int] reveal_type([x for x in range(10)]) -# revealed: set[Unknown | int] +# revealed: set[int] reveal_type({x for x in range(10)}) -# revealed: dict[Unknown | int, Unknown | str] +# revealed: dict[int, str] reveal_type({x: str(x) for x in range(10)}) -# revealed: list[Unknown | tuple[int, Unknown | str]] +# revealed: list[tuple[int, str]] reveal_type([(x, y) for x in range(5) for y in ["a", "b", "c"]]) squares: list[int | None] = [x**2 for x in range(10)] @@ -162,18 +162,17 @@ Inference for comprehensions takes the type context into account: from typing import Sequence # Without type context: -reveal_type([x for x in [1, 2, 3]]) # revealed: list[Unknown | int] -reveal_type({x: "a" for x in [1, 2, 3]}) # revealed: dict[Unknown | int, Unknown | str] -reveal_type({str(x): x for x in [1, 2, 3]}) # revealed: dict[Unknown | str, Unknown | int] -reveal_type({x for x in [1, 2, 3]}) # revealed: set[Unknown | int] +reveal_type([x for x in [1, 2, 3]]) # revealed: list[int] +reveal_type({x: "a" for x in [1, 2, 3]}) # revealed: dict[int, str] +reveal_type({str(x): x for x in [1, 2, 3]}) # revealed: dict[str, int] +reveal_type({x for x in [1, 2, 3]}) # revealed: set[int] # With type context: x1: list[int] = [x for x in [1, 2, 3]] reveal_type(x1) # revealed: list[int] x2: Sequence[int] = [x for x in [1, 2, 3]] -# TODO: This should reveal `list[int]`. -reveal_type(x2) # revealed: list[Unknown | int] +reveal_type(x2) # revealed: list[int] x3: dict[int, str] = {x: str(x) for x in [1, 2, 3]} reveal_type(x3) # revealed: dict[int, str] @@ -186,7 +185,7 @@ This also works for nested comprehensions: ```py table = [[(x, y) for x in range(3)] for y in range(3)] -reveal_type(table) # revealed: list[Unknown | list[Unknown | tuple[int, int]]] +reveal_type(table) # revealed: list[list[tuple[int, int]]] table_with_content: list[list[tuple[int, int, str | None]]] = [[(x, y, None) for x in range(3)] for y in range(3)] reveal_type(table_with_content) # revealed: list[list[tuple[int, int, str | None]]] @@ -216,9 +215,9 @@ y4: list[Person] = [{"misspelled": n} for n in ["Alice", "Bob"]] We promote literals to avoid overly-precise types in invariant positions: ```py -reveal_type([x for x in ("a", "b", "c")]) # revealed: list[Unknown | str] -reveal_type({x for x in (1, 2, 3)}) # revealed: set[Unknown | int] -reveal_type({k: 0 for k in ("a", "b", "c")}) # revealed: dict[Unknown | str, Unknown | int] +reveal_type([x for x in ("a", "b", "c")]) # revealed: list[str] +reveal_type({x for x in (1, 2, 3)}) # revealed: set[int] +reveal_type({k: 0 for k in ("a", "b", "c")}) # revealed: dict[str, int] ``` Type context can prevent this promotion from happening: diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 695639f38de79..d2aebe0f47734 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -152,7 +152,7 @@ class Cyclic: if isinstance(self.data, str): self.data = {"url": self.data} -# revealed: Unknown | str | dict[Unknown, Unknown] | dict[Unknown | str, Unknown | str] +# revealed: Unknown | str | dict[Unknown, Unknown] | dict[str, str] reveal_type(Cyclic("").data) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/del.md b/crates/ty_python_semantic/resources/mdtest/del.md index 945502ee826b2..6d6eaef21abdc 100644 --- a/crates/ty_python_semantic/resources/mdtest/del.md +++ b/crates/ty_python_semantic/resources/mdtest/del.md @@ -46,7 +46,7 @@ def delete(): del d # error: [unresolved-reference] "Name `d` used when not defined" delete() -reveal_type(d) # revealed: list[Unknown | int] +reveal_type(d) # revealed: list[int] def delete_element(): # When the `del` target isn't a name, it doesn't force local resolution. @@ -62,7 +62,7 @@ def delete_global(): delete_global() # Again, the variable should have been removed, but we don't check it. -reveal_type(d) # revealed: list[Unknown | int] +reveal_type(d) # revealed: list[int] def delete_nonlocal(): e = 2 diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index b8fb4c04962b6..ab1b6a0573d68 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -406,8 +406,8 @@ def head[T](my_list: MyList[T]) -> T: def get_value[K, V](my_dict: MyDict[K, V], key: K) -> V: return my_dict[key] -reveal_type(head([1, 2])) # revealed: Unknown | int -reveal_type(head(["a", "b"])) # revealed: Unknown | str +reveal_type(head([1, 2])) # revealed: int +reveal_type(head(["a", "b"])) # revealed: str d: dict[str, int] = {"a": 1} reveal_type(get_value(d, "a")) # revealed: int diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index a97eb8bf3ecd3..166d0d6a2a4ff 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -1033,7 +1033,7 @@ reveal_type(generic_context(c.generic_method)) reveal_type(c.generic_method) # revealed: [T](value: T) -> T reveal_type(c.generic_method(100)) # revealed: Literal[100] -reveal_type(c.generic_method([1, 2, 3])) # revealed: list[Unknown | int] +reveal_type(c.generic_method([1, 2, 3])) # revealed: list[int] ``` ## Callable protocols with `ParamSpec` and class constructors diff --git a/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md b/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md index 2dc3bf4839cf0..f84a9c747574e 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md +++ b/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md @@ -784,7 +784,7 @@ class A: ... from subexporter import * # TODO: we could potentially infer `list[str] | tuple[str, ...]` here -reveal_type(__all__) # revealed: list[Unknown | str] +reveal_type(__all__) # revealed: list[str] __all__.append("B") diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md index 9a1b077e0e23b..0376935bc8beb 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -9,13 +9,13 @@ reveal_type({}) # revealed: dict[Unknown, Unknown] ## Basic dict ```py -reveal_type({1: 1, 2: 1}) # revealed: dict[Unknown | int, Unknown | int] +reveal_type({1: 1, 2: 1}) # revealed: dict[int, int] ``` ## Dict of tuples ```py -reveal_type({1: (1, 2), 2: (3, 4)}) # revealed: dict[Unknown | int, Unknown | tuple[int, int]] +reveal_type({1: (1, 2), 2: (3, 4)}) # revealed: dict[int, tuple[int, int]] ``` ## Unpacked dict @@ -26,7 +26,7 @@ from typing import Mapping, KeysView a = {"a": 1, "b": 2} b = {"c": 3, "d": 4} c = {**a, **b} -reveal_type(c) # revealed: dict[Unknown | str, Unknown | int] +reveal_type(c) # revealed: dict[str, int] # revealed: list[int | str] # revealed: list[int | str] @@ -41,9 +41,9 @@ class HasKeysAndGetItem: return 42 def _(a: dict[str, int], b: Mapping[str, int], c: HasKeysAndGetItem, d: object): - reveal_type({**a}) # revealed: dict[Unknown | str, Unknown | int] - reveal_type({**b}) # revealed: dict[Unknown | str, Unknown | int] - reveal_type({**c}) # revealed: dict[Unknown | str, Unknown | int] + reveal_type({**a}) # revealed: dict[str, int] + reveal_type({**b}) # revealed: dict[str, int] + reveal_type({**c}) # revealed: dict[str, int] # error: [invalid-argument-type] "Argument expression after ** must be a mapping type: Found `object`" reveal_type({**d}) # revealed: dict[Unknown, Unknown] @@ -59,20 +59,20 @@ def b(_: int) -> int: return 1 x = {1: a, 2: b} -reveal_type(x) # revealed: dict[Unknown | int, Unknown | ((_: int) -> int)] +reveal_type(x) # revealed: dict[int, (_: int) -> int] ``` ## Mixed dict ```py -# revealed: dict[Unknown | str, Unknown | int | tuple[int, int] | tuple[int, int, int]] +# revealed: dict[str, int | tuple[int, int] | tuple[int, int, int]] reveal_type({"a": 1, "b": (1, 2), "c": (1, 2, 3)}) ``` ## Dict comprehensions ```py -# revealed: dict[Unknown | int, Unknown | int] +# revealed: dict[int, int] reveal_type({x: y for x, y in enumerate(range(42))}) ``` @@ -85,7 +85,7 @@ individual keys: from typing import TypedDict x1 = {"a": 1, "b": "2"} -reveal_type(x1) # revealed: dict[Unknown | str, Unknown | int | str] +reveal_type(x1) # revealed: dict[str, int | str] reveal_type(x1["a"]) # revealed: Literal[1] reveal_type(x1["b"]) # revealed: Literal["2"] @@ -107,7 +107,7 @@ reveal_type(x3[2]) # revealed: TD x4 = {"a": 1, "b": {"c": 2, "d": "3"}} reveal_type(x4["a"]) # revealed: Literal[1] -reveal_type(x4["b"]) # revealed: dict[Unknown | str, Unknown | int | str] +reveal_type(x4["b"]) # revealed: dict[str, int | str] reveal_type(x4["b"]["c"]) # revealed: Literal[2] reveal_type(x4["b"]["d"]) # revealed: Literal["3"] @@ -119,6 +119,6 @@ reveal_type(x5["b"]["d"]) # revealed: TD x6 = x7 = {"a": 1} # TODO: This should reveal `Literal[1]`. -reveal_type(x6["a"]) # revealed: Unknown | int -reveal_type(x7["a"]) # revealed: Unknown | int +reveal_type(x6["a"]) # revealed: int +reveal_type(x7["a"]) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md index f0c9341e5a752..4647724e861f0 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md @@ -9,7 +9,7 @@ reveal_type([]) # revealed: list[Unknown] ## List of tuples ```py -reveal_type([(1, 2), (3, 4)]) # revealed: list[Unknown | tuple[int, int]] +reveal_type([(1, 2), (3, 4)]) # revealed: list[tuple[int, int]] ``` ## List of functions @@ -22,24 +22,24 @@ def b(_: int) -> int: return 1 x = [a, b] -reveal_type(x) # revealed: list[Unknown | ((_: int) -> int)] +reveal_type(x) # revealed: list[(_: int) -> int] ``` The inferred `Callable` type is function-like, i.e. we can still access attributes like `__name__`: ```py -reveal_type(x[0].__name__) # revealed: Unknown | str +reveal_type(x[0].__name__) # revealed: str ``` ## Mixed list ```py -# revealed: list[Unknown | int | tuple[int, int] | tuple[int, int, int]] +# revealed: list[int | tuple[int, int] | tuple[int, int, int]] reveal_type([1, (1, 2), (1, 2, 3)]) ``` ## List comprehensions ```py -reveal_type([x for x in range(42)]) # revealed: list[Unknown | int] +reveal_type([x for x in range(42)]) # revealed: list[int] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md index 44f99550d2cb3..ee371e32ac851 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md @@ -3,13 +3,13 @@ ## Basic set ```py -reveal_type({1, 2}) # revealed: set[Unknown | int] +reveal_type({1, 2}) # revealed: set[int] ``` ## Set of tuples ```py -reveal_type({(1, 2), (3, 4)}) # revealed: set[Unknown | tuple[int, int]] +reveal_type({(1, 2), (3, 4)}) # revealed: set[tuple[int, int]] ``` ## Set of functions @@ -22,18 +22,18 @@ def b(_: int) -> int: return 1 x = {a, b} -reveal_type(x) # revealed: set[Unknown | ((_: int) -> int)] +reveal_type(x) # revealed: set[(_: int) -> int] ``` ## Mixed set ```py -# revealed: set[Unknown | int | tuple[int, int] | tuple[int, int, int]] +# revealed: set[int | tuple[int, int] | tuple[int, int, int]] reveal_type({1, (1, 2), (1, 2, 3)}) ``` ## Set comprehensions ```py -reveal_type({x for x in range(42)}) # revealed: set[Unknown | int] +reveal_type({x for x in range(42)}) # revealed: set[int] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md index ffb7d19e965cf..1443cffe44b76 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md @@ -65,9 +65,9 @@ reveal_type(promote(f)) # revealed: list[(_: int) -> int] The elements of invariant collection literals, i.e. lists, dictionaries, and sets, are promoted: ```py -reveal_type([1, 2, 3]) # revealed: list[Unknown | int] -reveal_type({"a": 1, "b": 2, "c": 3}) # revealed: dict[Unknown | str, Unknown | int] -reveal_type({"a", "b", "c"}) # revealed: set[Unknown | str] +reveal_type([1, 2, 3]) # revealed: list[int] +reveal_type({"a": 1, "b": 2, "c": 3}) # revealed: dict[str, int] +reveal_type({"a", "b", "c"}) # revealed: set[str] ``` Covariant collection literals are not promoted: @@ -146,7 +146,7 @@ reveal_type(x1) # revealed: tuple[tuple[tuple[Literal[1]]]] reveal_type(promote(x1)) # revealed: list[tuple[tuple[tuple[int]]]] x2 = ([1, 2], [(3,), (4,)], ["5", "6"]) -reveal_type(x2) # revealed: tuple[list[Unknown | int], list[Unknown | tuple[int]], list[Unknown | str]] +reveal_type(x2) # revealed: tuple[list[int], list[tuple[int]], list[str]] ``` However, this promotion should not take place if the literal type appears in contravariant position, @@ -158,7 +158,7 @@ def in_negated_position(non_zero_number: int): raise ValueError() reveal_type(non_zero_number) # revealed: int & ~Literal[0] - reveal_type([non_zero_number]) # revealed: list[Unknown | (int & ~Literal[0])] + reveal_type([non_zero_number]) # revealed: list[int & ~Literal[0]] ``` ## Literal annotations are respected @@ -389,15 +389,15 @@ def promote[T](x: T) -> list[T]: x1 = "hello" reveal_type(x1) # revealed: Literal["hello"] -reveal_type([x1]) # revealed: list[Unknown | str] +reveal_type([x1]) # revealed: list[str] x2: Literal["hello"] = "hello" reveal_type(x2) # revealed: Literal["hello"] -reveal_type([x2]) # revealed: list[Unknown | Literal["hello"]] +reveal_type([x2]) # revealed: list[Literal["hello"]] x3: tuple[Literal["hello"]] = ("hello",) reveal_type(x3) # revealed: tuple[Literal["hello"]] -reveal_type([x3]) # revealed: list[Unknown | tuple[Literal["hello"]]] +reveal_type([x3]) # revealed: list[tuple[Literal["hello"]]] def f() -> Literal["hello"]: return "hello" @@ -407,18 +407,18 @@ def id[T](x: T) -> T: reveal_type(f()) # revealed: Literal["hello"] reveal_type((f(),)) # revealed: tuple[Literal["hello"]] -reveal_type([f()]) # revealed: list[Unknown | Literal["hello"]] -reveal_type([id(f())]) # revealed: list[Unknown | Literal["hello"]] +reveal_type([f()]) # revealed: list[Literal["hello"]] +reveal_type([id(f())]) # revealed: list[Literal["hello"]] def _(x: tuple[Literal["hello"]]): reveal_type(x) # revealed: tuple[Literal["hello"]] - reveal_type([x]) # revealed: list[Unknown | tuple[Literal["hello"]]] + reveal_type([x]) # revealed: list[tuple[Literal["hello"]]] type X = Literal["hello"] x4: X = "hello" reveal_type(x4) # revealed: Literal["hello"] -reveal_type([x4]) # revealed: list[Unknown | Literal["hello"]] +reveal_type([x4]) # revealed: list[Literal["hello"]] class MyEnum(Enum): A = 1 @@ -427,7 +427,7 @@ class MyEnum(Enum): def _(x: Literal[MyEnum.A, MyEnum.B]): reveal_type(x) # revealed: Literal[MyEnum.A, MyEnum.B] - reveal_type([x]) # revealed: list[Unknown | Literal[MyEnum.A, MyEnum.B]] + reveal_type([x]) # revealed: list[Literal[MyEnum.A, MyEnum.B]] ``` Literal promotability is respected by unions: @@ -440,29 +440,29 @@ def _(flag: bool): unpromotable1: Literal["age"] | None = "age" if flag else None reveal_type(unpromotable1 or promotable1) # revealed: Literal["age"] - reveal_type([unpromotable1 or promotable1]) # revealed: list[Unknown | Literal["age"]] + reveal_type([unpromotable1 or promotable1]) # revealed: list[Literal["age"]] promotable2 = "age" if flag else None unpromotable2: Literal["age"] = "age" reveal_type(promotable2 or unpromotable2) # revealed: Literal["age"] - reveal_type([promotable2 or unpromotable2]) # revealed: list[Unknown | Literal["age"]] + reveal_type([promotable2 or unpromotable2]) # revealed: list[Literal["age"]] promotable3 = True unpromotable3: Literal[True] | None = True if flag else None reveal_type(unpromotable3 or promotable3) # revealed: Literal[True] - reveal_type([unpromotable3 or promotable3]) # revealed: list[Unknown | Literal[True]] + reveal_type([unpromotable3 or promotable3]) # revealed: list[Literal[True]] promotable4 = True if flag else None unpromotable4: Literal[True] = True reveal_type(promotable4 or unpromotable4) # revealed: Literal[True] - reveal_type([promotable4 or unpromotable4]) # revealed: list[Unknown | Literal[True]] + reveal_type([promotable4 or unpromotable4]) # revealed: list[Literal[True]] type X = Literal[b"bar"] def _(x1: X | None, x2: X): - reveal_type([x1, x2]) # revealed: list[Unknown | Literal[b"bar"] | None] - reveal_type([x1 or x2]) # revealed: list[Unknown | Literal[b"bar"]] + reveal_type([x1, x2]) # revealed: list[Literal[b"bar"] | None] + reveal_type([x1 or x2]) # revealed: list[Literal[b"bar"]] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/lists.md b/crates/ty_python_semantic/resources/mdtest/subscript/lists.md index d4026b1995df9..eeac45cdad6fb 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/lists.md @@ -9,11 +9,11 @@ A list can be indexed into with: ```py x = [1, 2, 3] -reveal_type(x) # revealed: list[Unknown | int] +reveal_type(x) # revealed: list[int] -reveal_type(x[0]) # revealed: Unknown | int +reveal_type(x[0]) # revealed: int -reveal_type(x[0:1]) # revealed: list[Unknown | int] +reveal_type(x[0:1]) # revealed: list[int] # error: [invalid-argument-type] reveal_type(x["a"]) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index ddba230a5140c..b851962d055a8 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -55,7 +55,7 @@ def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never], bb: LiskovUncom reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]] -reveal_type(tuple([1])) # revealed: tuple[Unknown | int, ...] +reveal_type(tuple([1])) # revealed: tuple[int, ...] x1: tuple[int, ...] = tuple([1]) reveal_type(x1) # revealed: tuple[int, ...] @@ -555,7 +555,7 @@ reveal_type((42, *[], 56, *[])) # revealed: tuple[Literal[42], Literal[56]] tup: Sequence[str] = (*{"foo": 42, "bar": 56},) # TODO: `tuple[str, str]` would be better, given the type annotation -reveal_type(tup) # revealed: tuple[Unknown | str, Unknown | str] +reveal_type(tup) # revealed: tuple[str, str] def f(x: list[int]): reveal_type((42, 56, *x, 97)) # revealed: tuple[Literal[42], Literal[56], *tuple[int, ...], Literal[97]] diff --git a/crates/ty_python_semantic/resources/mdtest/unpacking.md b/crates/ty_python_semantic/resources/mdtest/unpacking.md index 31116647e0334..b79c6db3f6569 100644 --- a/crates/ty_python_semantic/resources/mdtest/unpacking.md +++ b/crates/ty_python_semantic/resources/mdtest/unpacking.md @@ -213,8 +213,8 @@ reveal_type(d) # revealed: Literal[2] ```py a, b = [1, 2] -reveal_type(a) # revealed: Unknown | int -reveal_type(b) # revealed: Unknown | int +reveal_type(a) # revealed: int +reveal_type(b) # revealed: int ``` ### Simple unpacking diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 29f34170b1a91..98634d54f3a2e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10313,13 +10313,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } - // If a valid type annotation was not provided, avoid restricting the type of the - // collection by unioning the inferred type with `Unknown`. - let elt_tcx = elt_tcx.unwrap_or(Type::unknown()); - - builder - .infer(&constraints, Type::TypeVar(elt_ty), elt_tcx) - .ok()?; + // If there is no applicable context for this element type variable, we infer from the + // literal elements directly. This violates the gradual guarantee (we don't know that + // our inference is compatible with subsequent additions to the collection), but it + // matches the behavior of other type checkers and is usually the desired behavior. + if let Some(elt_tcx) = elt_tcx { + builder + .infer(&constraints, Type::TypeVar(elt_ty), elt_tcx) + .ok()?; + } } for elts in elts { diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index a60447113fa8d..e3b4564c8682e 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -71,7 +71,7 @@ async def clone(self: Self, checkout_dir: Path) -> AsyncIterator[str]: git_clone_command.extend( [ f"https://github.com/{self.org}/{self.repo}", - checkout_dir, + str(checkout_dir), ], ) diff --git a/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py b/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py index 7ce226507913f..d5d94ffbba141 100644 --- a/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py +++ b/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py @@ -28,7 +28,13 @@ Pyrefly(), ] -SEVERITY_LABELS: Final = {1: "Error", 2: "Warning", 3: "Info", 4: "Hint"} +SEVERITY_LABELS: Final = { + None: "Unknown", + 1: "Error", + 2: "Warning", + 3: "Info", + 4: "Hint", +} @pytest.fixture(scope="module", params=ALL_PROJECTS, ids=lambda p: p.name) From 94c0a20e3c5b1d9f221b6af800c1e9f649c77388 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 5 Mar 2026 16:16:15 -0800 Subject: [PATCH 222/261] [ty] eliminate negative intersection elements in promotion (#23750) ## Summary Avoid inferring invariant container types like `list[int & ~AlwaysFalsy]`, preferring `list[int]` instead. Also generally rename "literal promotion" to just "promotion" -- it already promoted e.g. `float` to `int | float`, so it wasn't restricted to literal types. Now it includes even more non-literal cases. ## Test Plan Added an mdtest. Removes ~200 ecosystem hits from https://github.com/astral-sh/ruff/pull/23718 --- .../resources/mdtest/bidirectional.md | 35 ++++------ .../{literal_promotion.md => promotion.md} | 67 ++++++++++++++----- crates/ty_python_semantic/src/types.rs | 49 ++++++++------ .../ty_python_semantic/src/types/call/bind.rs | 4 +- .../src/types/diagnostic.rs | 2 +- .../ty_python_semantic/src/types/generics.rs | 2 +- .../src/types/ide_support.rs | 8 +-- .../src/types/infer/builder.rs | 10 +-- .../src/types/known_instance.rs | 2 +- .../ty_python_semantic/src/types/literal.rs | 4 +- .../ty_python_semantic/src/types/typevar.rs | 2 +- 11 files changed, 108 insertions(+), 77 deletions(-) rename crates/ty_python_semantic/resources/mdtest/{literal_promotion.md => promotion.md} (85%) diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index c4ffe8316e363..502156865748e 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -91,34 +91,25 @@ from typing import overload, Callable def list1[T](x: T) -> list[T]: return [x] -def get_data() -> dict | None: - return {} +def f() -> list[object]: + reveal_type(list1(1)) # revealed: list[int] + # `list[int]` and `list[object]` are incompatible, but the return type check passes here + # because the type of `list1(res)` is inferred by bidirectional type inference using the + # annotated return type, and the type of `res` is not used. + return list1(1) -def wrap_data() -> list[dict]: - if not (res := get_data()): - return list1({}) - reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown] & ~AlwaysFalsy] - # `list[dict[Unknown, Unknown] & ~AlwaysFalsy]` and `list[dict[Unknown, Unknown]]` are incompatible, - # but the return type check passes here because the type of `list1(res)` is inferred - # by bidirectional type inference using the annotated return type, and the type of `res` is not used. - return list1(res) - -def wrap_data2() -> list[dict] | None: - if not (res := get_data()): - return None - reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown] & ~AlwaysFalsy] - return list1(res) +def f2() -> list[object] | None: + reveal_type(list1(1)) # revealed: list[int] + return list1(1) def deco[T](func: Callable[[], T]) -> Callable[[], T]: return func -def outer() -> Callable[[], list[dict]]: +def outer() -> Callable[[], list[object]]: @deco - def inner() -> list[dict]: - if not (res := get_data()): - return list1({}) - reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown] & ~AlwaysFalsy] - return list1(res) + def inner() -> list[object]: + reveal_type(list1(1)) # revealed: list[int] + return list1(1) return inner @overload diff --git a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md b/crates/ty_python_semantic/resources/mdtest/promotion.md similarity index 85% rename from crates/ty_python_semantic/resources/mdtest/literal_promotion.md rename to crates/ty_python_semantic/resources/mdtest/promotion.md index 1443cffe44b76..1dd0593822987 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/promotion.md @@ -1,14 +1,27 @@ -# Literal promotion +# Type promotion ```toml [environment] python-version = "3.12" ``` -There are certain places where we promote literals to their common supertype. +There are certain places (usually when inferring a type for a typevar in an invariant position) +where we "promote" types to a supertype, rather than inferring the most precise possible types. For +example, we don't want `[1, 2]` to be inferred as `list[Literal[1, 2]]`, since that would prevent +adding a `3` to the list later; we prefer `list[int]` instead. -We also promote `float` to `int | float` and `complex` to `int | float | complex`, even when not in -a type annotation. +This is a heuristic, where we are trying to guess the type the user probably means, in the absence +of a clarifying annotation, and in place of trying to do global inference that accounts for every +use up-front. + +In addition to promoting literal types to their nominal supertype (e.g. `Literal[1]` to `int`, +`Literal["foo"]` to `str`, we also promote `float` to `int | float` and `complex` to +`int | float | complex`. + +We also remove negative intersection elements, so that e.g. `A & ~AlwaysFalsy` promotes to simply +`A`. + +We avoid promoting literal types that originate from an explicit annotation. ## Implicitly inferred literal types are promotable @@ -79,8 +92,8 @@ reveal_type(frozenset((1, 2, 3))) # revealed: frozenset[Literal[1, 2, 3]] ## Invariant and contravariant return types are promoted -Literals are promoted if they are in non-covariant position in the return type of a generic -function, or constructor of a generic class: +We promote in non-covariant position in the return type of a generic function, or constructor of a +generic class: ```py class Bivariant[T]: @@ -133,7 +146,7 @@ reveal_type(f10(1, 1)) # revealed: tuple[Invariant[int], Covariant[Literal[1]]] reveal_type(f11(1, 1)) # revealed: tuple[Invariant[Covariant[int] | None], Covariant[Literal[1]]] | None ``` -## Literals are promoted recursively +## Promotion is recursive ```py from typing import Literal @@ -149,16 +162,24 @@ x2 = ([1, 2], [(3,), (4,)], ["5", "6"]) reveal_type(x2) # revealed: tuple[list[int], list[tuple[int]], list[str]] ``` -However, this promotion should not take place if the literal type appears in contravariant position, -e.g., the negative member of a covariant intersection type: +However, this promotion should not take place in contravariant position: ```py -def in_negated_position(non_zero_number: int): - if non_zero_number == 0: - raise ValueError() +from typing import Generic, TypeVar +from ty_extensions import Intersection, Not, AlwaysFalsy - reveal_type(non_zero_number) # revealed: int & ~Literal[0] - reveal_type([non_zero_number]) # revealed: list[int & ~Literal[0]] +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + +class A: ... +class Consumer(Generic[T_contra]): ... +class Producer(Generic[T_co]): ... + +def _(c: Consumer[Intersection[A, Not[AlwaysFalsy]]], p: Producer[Intersection[A, Not[AlwaysFalsy]]]): + reveal_type(c) # revealed: Consumer[A & ~AlwaysFalsy] + reveal_type(p) # revealed: Producer[A & ~AlwaysFalsy] + reveal_type([c]) # revealed: list[Consumer[A & ~AlwaysFalsy]] + reveal_type([p]) # revealed: list[Producer[A]] ``` ## Literal annotations are respected @@ -320,8 +341,8 @@ reveal_type(x12) # revealed: Sub2[int, Literal[2]] ## Constrained TypeVars with Literal constraints -Literal promotion should not apply to constrained TypeVars, since the inferred type is already one -of the constraints. Promoting it would produce a type that doesn't match any constraint. +Promotion should not apply to constrained TypeVars, since the inferred type is already one of the +constraints. Promoting it would produce a type that doesn't match any constraint. ```py from typing import TypeVar, Literal, Generic @@ -466,3 +487,17 @@ def _(x1: X | None, x2: X): reveal_type([x1, x2]) # revealed: list[Literal[b"bar"] | None] reveal_type([x1 or x2]) # revealed: list[Literal[b"bar"]] ``` + +## Negative intersection elements are removed + +Truthiness narrowing should not leak into invariant literal container inference: + +```py +class A: ... + +def _(a: A | None): + if a: + d = {"a": a} + reveal_type(d) # revealed: dict[str, A] + return {} +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 77b852046dae4..f7913e7715379 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1694,16 +1694,16 @@ impl<'db> Type<'db> { /// Note that this function tries to promote literals to a more user-friendly form than their /// fallback instance type. For example, `def _() -> int` is promoted to `Callable[[], int]`, /// as opposed to `FunctionType`. - pub(crate) fn promote_literals(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn promote(self, db: &'db dyn Db) -> Type<'db> { self.apply_type_mapping( db, - &TypeMapping::PromoteLiterals(PromoteLiteralsMode::On), + &TypeMapping::Promote(PromotionMode::On), TypeContext::default(), ) } - /// Like [`Type::promote_literals`], but does not recurse into nested types. - fn promote_literals_impl(self, db: &'db dyn Db) -> Type<'db> { + /// Like [`Type::promote`], but does not recurse into nested types. + fn promote_impl(self, db: &'db dyn Db) -> Type<'db> { match self { Type::LiteralValue(literal) if literal.is_promotable() => literal.fallback_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_instance(db), @@ -5207,14 +5207,14 @@ impl<'db> Type<'db> { match type_mapping { // Promote the types within the signature before promoting the signature to its // callable form. - TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => { + TypeMapping::Promote(PromotionMode::On) => { Type::FunctionLiteral(function.apply_type_mapping_impl( db, type_mapping, tcx, visitor, )) - .promote_literals_impl(db) + .promote_impl(db) } _ => Type::FunctionLiteral(function.apply_type_mapping_impl( db, @@ -5231,7 +5231,7 @@ impl<'db> Type<'db> { method.self_instance(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), )), - Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::PromoteLiterals(PromoteLiteralsMode::On)) => { + Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::Promote(PromotionMode::On)) => { match instance.known_class(db) { Some(KnownClass::Complex) => KnownUnion::Complex.to_type(db), Some(KnownClass::Float) => KnownUnion::Float.to_type(db), @@ -5311,9 +5311,14 @@ impl<'db> Type<'db> { builder = builder.add_positive(positive.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); } - for negative in intersection.negative(db) { - builder = - builder.add_negative(negative.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor)); + // Promotion should remove negative contributions from intersections, + // so we don't preserve them here when promotion is enabled. + if !matches!(type_mapping, TypeMapping::Promote(PromotionMode::On)) { + for negative in intersection.negative(db) { + builder = builder.add_negative( + negative.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor), + ); + } } builder.build() } @@ -5380,8 +5385,8 @@ impl<'db> Type<'db> { TypeMapping::ReplaceParameterDefaults | TypeMapping::EagerExpansion | TypeMapping::RescopeReturnCallables(_) | - TypeMapping::PromoteLiterals(PromoteLiteralsMode::Off) => self, - TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => self.promote_literals_impl(db) + TypeMapping::Promote(PromotionMode::Off) => self, + TypeMapping::Promote(PromotionMode::On) => self.promote_impl(db) } Type::LiteralValue(_) => match type_mapping { @@ -5395,8 +5400,8 @@ impl<'db> Type<'db> { TypeMapping::ReplaceParameterDefaults | TypeMapping::EagerExpansion | TypeMapping::RescopeReturnCallables(_) | - TypeMapping::PromoteLiterals(PromoteLiteralsMode::Off) => self, - TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => self.promote_literals_impl(db), + TypeMapping::Promote(PromotionMode::Off) => self, + TypeMapping::Promote(PromotionMode::On) => self.promote_impl(db), } Type::Dynamic(_) => match type_mapping { @@ -5406,7 +5411,7 @@ impl<'db> Type<'db> { TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf(..) | TypeMapping::ReplaceSelf { .. } | - TypeMapping::PromoteLiterals(_) | + TypeMapping::Promote(_) | TypeMapping::ReplaceParameterDefaults | TypeMapping::EagerExpansion | TypeMapping::RescopeReturnCallables(_) => self, @@ -6052,16 +6057,16 @@ impl<'db> VarianceInferable<'db> for Type<'db> { } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] -pub enum PromoteLiteralsMode { +pub enum PromotionMode { On, Off, } -impl PromoteLiteralsMode { +impl PromotionMode { const fn flip(self) -> Self { match self { - PromoteLiteralsMode::On => PromoteLiteralsMode::Off, - PromoteLiteralsMode::Off => PromoteLiteralsMode::On, + PromotionMode::On => PromotionMode::Off, + PromotionMode::Off => PromotionMode::On, } } } @@ -6180,7 +6185,7 @@ pub enum TypeMapping<'a, 'db> { }, /// Replaces any literal types with their corresponding promoted type form (e.g. `Literal["string"]` /// to `str`, or `def _() -> int` to `Callable[[], int]`). - PromoteLiterals(PromoteLiteralsMode), + Promote(PromotionMode), /// Binds a legacy typevar with the generic context (class, function, type alias) that it is /// being used in. BindLegacyTypevars(BindingContext<'db>), @@ -6230,7 +6235,7 @@ impl<'db> TypeMapping<'_, 'db> { ) } TypeMapping::UniqueSpecialization { .. } - | TypeMapping::PromoteLiterals(_) + | TypeMapping::Promote(_) | TypeMapping::BindLegacyTypevars(_) | TypeMapping::Materialize(_) | TypeMapping::ReplaceParameterDefaults @@ -6273,7 +6278,7 @@ impl<'db> TypeMapping<'_, 'db> { specialization: specialization.clone(), materialization_kind: materialization_kind.flip(), }, - TypeMapping::PromoteLiterals(mode) => TypeMapping::PromoteLiterals(mode.flip()), + TypeMapping::Promote(mode) => TypeMapping::Promote(mode.flip()), TypeMapping::ApplySpecialization(_) | TypeMapping::UniqueSpecialization { .. } | TypeMapping::BindLegacyTypevars(_) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 15f55c7b0c61c..a1019b390f7ac 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3797,7 +3797,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { self.errors.extend(specialization_errors); - // Attempt to promote any literal types assigned to the specialization. + // Attempt to promote any promotable types assigned to the specialization. let maybe_promote = |typevar: BoundTypeVarInstance<'db>, ty: Type<'db>| { let bound_or_constraints = typevar.typevar(self.db).bound_or_constraints(self.db); @@ -3831,7 +3831,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return ty; } - let promoted = ty.promote_literals(self.db); + let promoted = ty.promote(self.db); // If the TypeVar has an upper bound, only use the promoted type if it // still satisfies the bound. diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 20622861afbc7..1209d9a8c808b 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -4477,7 +4477,7 @@ pub(crate) fn report_undeclared_protocol_member( if definition.kind(db).is_unannotated_assignment() { let binding_type = binding_type(db, definition); - let suggestion = binding_type.promote_literals(db); + let suggestion = binding_type.promote(db); if should_give_hint(db, suggestion) { diagnostic.set_primary_message(format_args!( diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index f02abde6ae80d..f4c08d30943d0 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1830,7 +1830,7 @@ impl<'db> SpecializationBuilder<'db> { /// the specialization that this builder is building up. /// /// `formal` should be the top-level formal parameter type that we are inferring. This is used - /// by our literal promotion logic, which needs to know which typevars are affected by each + /// by our promotion logic, which needs to know which typevars are affected by each /// argument, and the variance of those typevars in the corresponding parameter. /// /// TODO: This is a stopgap! Eventually, the builder will maintain a single constraint set for diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 5a545310b3893..5eb5e6249789f 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -769,7 +769,7 @@ pub fn definitions_for_bin_op<'db>( return None; }; - let callable_type = promote_literals_for_self(model.db(), bindings.callable_type()); + let callable_type = promote_for_self(model.db(), bindings.callable_type()); let definitions: Vec<_> = bindings .iter_flat() @@ -827,7 +827,7 @@ pub fn definitions_for_unary_op<'db>( ) => *bindings, }; - let callable_type = promote_literals_for_self(model.db(), bindings.callable_type()); + let callable_type = promote_for_self(model.db(), bindings.callable_type()); let definitions = bindings .iter_flat() @@ -842,10 +842,10 @@ pub fn definitions_for_unary_op<'db>( Some((definitions, callable_type)) } -/// Promotes literal types in `self` positions to their fallback instance types. +/// Promotes types in `self` positions. /// /// This is so that we show e.g. `int.__add__` instead of `Literal[4].__add__`. -fn promote_literals_for_self<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { +fn promote_for_self<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match ty { Type::BoundMethod(method) => Type::BoundMethod(method.map_self_type(db, |self_ty| { self_ty.literal_fallback_instance(db).unwrap_or(self_ty) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 98634d54f3a2e..7ff5c6cb6d6d7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -9971,7 +9971,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if tuple.len() > MAX_TUPLE_LENGTH_FOR_UNANNOTATED_LITERAL_INFERENCE { // Promote literals for very large unannotated tuples, // to avoid pathological performance issues - self.infer_expression(elt, ctx).promote_literals(db) + self.infer_expression(elt, ctx).promote(db) } else { self.infer_expression(elt, ctx) } @@ -10367,7 +10367,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Note that unlike when preferring the declared type, we use covariant type // assignments from the type context to potentially _narrow_ the inferred type, - // by avoiding literal promotion. + // by avoiding promotion. let elt_ty_identity = elt_ty.identity(self.db()); // If the element is a starred expression, we want to apply the type context to each element @@ -10395,9 +10395,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } - // Convert any element literals to their promoted type form to avoid excessively large - // unions for large nested list literals, which the constraint solver struggles with. - let inferred_elt_ty = inferred_elt_ty.promote_literals(self.db()); + // Promote types to avoid excessively large unions for large nested list literals, + // which the constraint solver struggles with. + let inferred_elt_ty = inferred_elt_ty.promote(self.db()); builder .infer( diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 41ed564beb454..8bd62585760e9 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -285,7 +285,7 @@ impl<'db> KnownInstanceType<'db> { TypeMapping::ApplySpecialization(_) | TypeMapping::ApplySpecializationWithMaterialization { .. } | TypeMapping::UniqueSpecialization { .. } - | TypeMapping::PromoteLiterals(_) + | TypeMapping::Promote(_) | TypeMapping::BindSelf(..) | TypeMapping::ReplaceSelf { .. } | TypeMapping::Materialize(_) diff --git a/crates/ty_python_semantic/src/types/literal.rs b/crates/ty_python_semantic/src/types/literal.rs index 68ce81262c379..9f6839f5e5167 100644 --- a/crates/ty_python_semantic/src/types/literal.rs +++ b/crates/ty_python_semantic/src/types/literal.rs @@ -55,7 +55,7 @@ impl<'db> LiteralValueType<'db> { } } - /// Creates a literal value that may be promoted during literal promotion. + /// Creates a literal value that may be promoted. pub(crate) fn promotable(kind: impl Into>) -> LiteralValueType<'db> { let repr = match kind.into() { LiteralValueTypeKind::Int(int) => LiteralValueTypeInner::PromotableInt(int), @@ -69,7 +69,7 @@ impl<'db> LiteralValueType<'db> { Self(repr) } - /// Creates a literal value that should not be promoted during literal promotion. + /// Creates a literal value that should not be promoted. pub(crate) fn unpromotable( kind: impl Into>, ) -> LiteralValueType<'db> { diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 7621e266968d0..bca9d7f8e43c7 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -904,7 +904,7 @@ impl<'db> BoundTypeVarInstance<'db> { } } TypeMapping::UniqueSpecialization { .. } - | TypeMapping::PromoteLiterals(_) + | TypeMapping::Promote(_) | TypeMapping::ReplaceParameterDefaults | TypeMapping::BindLegacyTypevars(_) | TypeMapping::EagerExpansion From 07286ef981d6726ea0bb8d6dd4acbf224d4d9344 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Mar 2026 21:16:58 -0500 Subject: [PATCH 223/261] [ty] Reject `type[Callable]` special form (#23753) ## Summary The [typing spec](https://typing.python.org/en/latest/spec/special-types.html) says: > Any other [special forms](https://typing.python.org/en/latest/spec/glossary.html#term-special-form) like Callable are not allowed as an argument to type. We already reject `Generic` and `TypedDict`. We should probably also reject `Literal`. But this PR adds `Callable`. Closes https://github.com/astral-sh/ty/issues/2964. --- .../resources/mdtest/type_of/basic.md | 17 +++++++++++ .../types/infer/builder/type_expression.rs | 30 ++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md index 4ebc65edea726..c7e829b2b0392 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -165,6 +165,23 @@ class B: ... _: type[A, B] ``` +## Callable types are not valid parameters + +```py +from collections.abc import Callable + +def f( + x: type[Callable], # error: [invalid-type-form] + y: type[Callable[[int], str]], # error: [invalid-type-form] + # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments" + # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" + z: type[Callable[int]], # error: [invalid-type-form] "The argument to `type[]` must be a class object type" +): + reveal_type(x) # revealed: type[Unknown] + reveal_type(y) # revealed: type[Unknown] + reveal_type(z) # revealed: type[Unknown] +``` + ## As a base class ```py diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index c62d9a49ab2bc..56e882c7c0b33 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -777,11 +777,26 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// Given the slice of a `type[]` annotation, return the type that the annotation represents fn infer_subclass_of_type_expression(&mut self, slice: &ast::Expr) -> Type<'db> { + let invalid_type_argument = |builder: &Self, slice: &ast::Expr| { + builder.report_invalid_type_expression( + slice, + "The argument to `type[]` must be a class object type", + ); + SubclassOfType::subclass_of_unknown() + }; + + let infer_type_argument = |builder: &mut Self, slice: &ast::Expr| { + let slice_ty = builder.infer_type_expression(slice); + SubclassOfType::try_from_instance(builder.db(), slice_ty).unwrap_or_else(|| { + match slice_ty { + Type::Callable(_) => invalid_type_argument(builder, slice), + _ => todo_type!("unsupported type[X] special form"), + } + }) + }; + match slice { - ast::Expr::Name(_) | ast::Expr::Attribute(_) => { - SubclassOfType::try_from_instance(self.db(), self.infer_type_expression(slice)) - .unwrap_or(todo_type!("unsupported type[X] special form")) - } + ast::Expr::Name(_) | ast::Expr::Attribute(_) => infer_type_argument(self, slice), ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => { let union_ty = UnionType::from_elements_leave_aliases( self.db(), @@ -866,6 +881,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } } + Type::SpecialForm(special_form @ SpecialFormType::Callable) => { + self.infer_parameterized_special_form_type_expression( + subscript, + special_form, + ); + invalid_type_argument(self, slice) + } _ => { self.infer_type_expression(parameters); todo_type!("unsupported nested subscript in type[X]") From 6920ea90b14374d05c32b3b1d075cd135cd23d21 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 6 Mar 2026 01:04:38 -0500 Subject: [PATCH 224/261] [ty] Support place narrowing for dictionaries contained within list/tuple literals (#23569) Resolves https://github.com/astral-sh/ty/issues/2894 by traversing list (or tuple) literals that may contain nested dictionary literals. --- .../mdtest/literal/collections/dictionary.md | 24 ++++++++ .../src/semantic_index/builder.rs | 61 ++++++++++++++----- 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md index 0376935bc8beb..29ad96141351d 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -121,4 +121,28 @@ x6 = x7 = {"a": 1} # TODO: This should reveal `Literal[1]`. reveal_type(x6["a"]) # revealed: int reveal_type(x7["a"]) # revealed: int + +x8: list[dict[str, int | str]] = [{"a": 1, "b": "2"}, {"a": 3, "b": "4"}] +reveal_type(x8[0]["a"]) # revealed: Literal[1] +reveal_type(x8[1]["b"]) # revealed: Literal["4"] + +x9: dict[str, list[dict[str, int | str]]] = {"a": [{"a": 1, "b": "2"}, {"a": 3, "b": "4"}]} +reveal_type(x9["a"][0]["a"]) # revealed: Literal[1] +reveal_type(x9["a"][1]["b"]) # revealed: Literal["4"] + +x10: tuple[dict[str, int | str], ...] = ({"a": 1, "b": "2"}, {"a": 3, "b": "4"}) +reveal_type(x10[0]["a"]) # revealed: Literal[1] +reveal_type(x10[1]["b"]) # revealed: Literal["4"] + +x11: dict[str, tuple[dict[str, int | str], ...]] = {"a": ({"a": 1, "b": "2"}, {"a": 3, "b": "4"})} +reveal_type(x11["a"][0]["a"]) # revealed: Literal[1] +reveal_type(x11["a"][1]["b"]) # revealed: Literal["4"] + +x12 = [({"a": 1, "b": "2"}, {"a": 3, "b": "4"}, *[{"a": 5}], {"a": 6})] +reveal_type(x12[0][0]["a"]) # revealed: Literal[1] +reveal_type(x12[0][1]["b"]) # revealed: Literal["4"] + +# Starred expressions and any elements that follow them are not narrowed. +reveal_type(x12[0][2]["a"]) # revealed: int +reveal_type(x12[0][3]["b"]) # revealed: int ``` diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index ad6a5fe02f8d1..8498c2ffe100f 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -11,7 +11,7 @@ use ruff_db::source::{SourceText, source_text}; use ruff_index::IndexVec; use ruff_python_ast::name::Name; use ruff_python_ast::visitor::{Visitor, walk_expr, walk_pattern, walk_stmt}; -use ruff_python_ast::{self as ast, NodeIndex, PySourceType, PythonVersion}; +use ruff_python_ast::{self as ast, AtomicNodeIndex, NodeIndex, PySourceType, PythonVersion}; use ruff_python_parser::semantic_errors::{ SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind, YieldOutsideFunctionKind, @@ -793,7 +793,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { fn add_dict_key_assignment_definitions( &mut self, targets: impl IntoIterator + Copy, - dict: &'ast ast::ExprDict, + dict: &'ast ast::Expr, assignment: Definition<'db>, ) { // TODO: Although we synthesize place expressions for each dictionary key, the definition @@ -804,16 +804,45 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { }; if let Some(target) = MemberExprBuilder::visit_expr(target.into()) { - self.add_dict_key_assignment_definitions_impl(&target, dict, assignment); + self.add_dict_key_assignment_definitions_impl(&target, dict.into(), assignment); } } fn add_dict_key_assignment_definitions_impl( &mut self, target: &MemberExprBuilder, - dict: &'ast ast::ExprDict, + expr: ast::ExprRef<'ast>, assignment: Definition<'db>, ) { + let ruff_python_ast::ExprRef::Dict(dict) = expr else { + let items = match expr { + ruff_python_ast::ExprRef::List(list) => &list.elts, + ruff_python_ast::ExprRef::Tuple(tuple) => &tuple.elts, + _ => return, + }; + + // Traverse into nested collections that may contain dictionary literals. + for (i, item) in items + .iter() + // Ignore starred expressions and any elements that follow them, as we cannot + // determine the index to narrow on. + .take_while(|e| !e.is_starred_expr()) + .enumerate() + { + if let Some(target) = MemberExprBuilder::visit_subscript_expr( + target.clone(), + &ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(ast::Int::from(i as u64)), + range: TextRange::default(), + node_index: AtomicNodeIndex::NONE, + }), + ) { + self.add_dict_key_assignment_definitions_impl(&target, item.into(), assignment); + } + } + return; + }; + for item in &dict.items { let Some(key) = item.key.as_ref() else { continue; @@ -825,9 +854,11 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { }; // Recurse into nested dictionaries. - if let ast::Expr::Dict(dict_value) = &item.value { - self.add_dict_key_assignment_definitions_impl(&member_expr, dict_value, assignment); - } + self.add_dict_key_assignment_definitions_impl( + &member_expr, + (&item.value).into(), + assignment, + ); if let Some(place_expr) = PlaceExpr::try_from_member_expr(member_expr) { let place_id = self.add_place(place_expr); @@ -2863,13 +2894,11 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { }, ); - if let ast::Expr::Dict(dict) = &*node.value { - self.add_dict_key_assignment_definitions( - &node.targets, - dict, - assignment, - ); - } + self.add_dict_key_assignment_definitions( + &node.targets, + &node.value, + assignment, + ); } Some(CurrentAssignment::AnnAssign(ann_assign)) => { self.add_standalone_type_expression(&ann_assign.annotation); @@ -2883,10 +2912,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { }, ); - if let Some(ast::Expr::Dict(dict)) = ann_assign.value.as_deref() { + if let Some(value) = ann_assign.value.as_deref() { self.add_dict_key_assignment_definitions( [&*ann_assign.target], - dict, + value, assignment, ); } From 546d3b2b1ad214976c5238174efcb061df548207 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 6 Mar 2026 09:18:37 +0100 Subject: [PATCH 225/261] [ty] Fix overriding `all` selector (#23712) --- crates/ty/tests/cli/rule_selection.rs | 89 ++++++++++++++++++++++- crates/ty_combine/src/lib.rs | 30 ++++---- crates/ty_project/src/metadata/options.rs | 4 + 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs index 4f1af154c1e2b..cac5fc90feb35 100644 --- a/crates/ty/tests/cli/rule_selection.rs +++ b/crates/ty/tests/cli/rule_selection.rs @@ -969,7 +969,7 @@ fn cli_all_rules_warn() -> anyhow::Result<()> { /// The "all" keyword can be overridden by subsequent specific rule settings #[test] -fn cli_all_rules_with_override() -> anyhow::Result<()> { +fn cli_all_rules_precedence() -> anyhow::Result<()> { let case = CliTest::with_file( "test.py", r#" @@ -1218,3 +1218,90 @@ fn overrides_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> { Ok(()) } + +/// Tests the `all` selector in an `overrides` section +#[test] +fn all_overrides() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + all = "error" + + [[tool.ty.overrides]] + include = ["tests/**"] + + [tool.ty.overrides.rules] + unresolved-reference = "warn" + "#, + ), + ( + "main.py", + r#" + y = 4 / 0 # division-by-zero: error (global) + x = 1 + prin(x) # unresolved-reference: error (global) + "#, + ), + ( + "tests/test_main.py", + r#" + y = 4 / 0 # division-by-zero: error (global) + x = 1 + prin(x) # unresolved-reference: warn (override) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @" + success: false + exit_code: 1 + ----- stdout ----- + error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> main.py:2:5 + | + 2 | y = 4 / 0 # division-by-zero: error (global) + | ^^^^^ + 3 | x = 1 + 4 | prin(x) # unresolved-reference: error (global) + | + info: rule `division-by-zero` was selected in the configuration file + + error[unresolved-reference]: Name `prin` used when not defined + --> main.py:4:1 + | + 2 | y = 4 / 0 # division-by-zero: error (global) + 3 | x = 1 + 4 | prin(x) # unresolved-reference: error (global) + | ^^^^ + | + info: rule `unresolved-reference` was selected in the configuration file + + error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> tests/test_main.py:2:5 + | + 2 | y = 4 / 0 # division-by-zero: error (global) + | ^^^^^ + 3 | x = 1 + 4 | prin(x) # unresolved-reference: warn (override) + | + info: rule `division-by-zero` was selected in the configuration file + + warning[unresolved-reference]: Name `prin` used when not defined + --> tests/test_main.py:4:1 + | + 2 | y = 4 / 0 # division-by-zero: error (global) + 3 | x = 1 + 4 | prin(x) # unresolved-reference: warn (override) + | ^^^^ + | + info: rule `unresolved-reference` was selected in the configuration file + + Found 4 diagnostics + + ----- stderr ----- + "); + + Ok(()) +} diff --git a/crates/ty_combine/src/lib.rs b/crates/ty_combine/src/lib.rs index cced6ce6b6b89..248ea73dfc37c 100644 --- a/crates/ty_combine/src/lib.rs +++ b/crates/ty_combine/src/lib.rs @@ -3,11 +3,10 @@ reason = "Prefer System trait methods over std methods in ty crates" )] -use std::{collections::HashMap, hash::BuildHasher}; - use ordermap::OrderMap; use ruff_db::system::SystemPathBuf; use ruff_python_ast::PythonVersion; +use std::{collections::HashMap, hash::BuildHasher}; /// Combine two values, preferring the values in `self`. /// @@ -121,9 +120,18 @@ where K: Eq + std::hash::Hash, S: BuildHasher, { - fn combine_with(&mut self, other: Self) { + fn combine_with(&mut self, mut other: Self) { + // `self` takes precedence over `other` but values with higher precedence must be placed after. + // Swap the vectors so that `other` is the one that gets extended, so that the values of `self` come after. + std::mem::swap(self, &mut other); + for (k, v) in other { - self.entry(k).or_insert(v); + // If there's an existing entry, remove it to ensure `k` (previously self) + // comes after any item in `self`. + self.remove(&k); + + // Append `k` at the end. + self.insert(k, v); } } } @@ -208,21 +216,15 @@ mod tests { #[test] fn combine_order_map() { - let a: OrderMap = OrderMap::from_iter([(1, "a"), (2, "a"), (3, "a")]); - let b: OrderMap = OrderMap::from_iter([(0, "b"), (2, "b"), (5, "b")]); + let a: OrderMap<_, _> = OrderMap::from_iter([(1, "a"), (2, "a"), (3, "a")]); + let b: OrderMap<_, _> = OrderMap::from_iter([(0, "b"), (2, "b"), (5, "b")]); assert_eq!(None.combine(Some(b.clone())), Some(b.clone())); assert_eq!(Some(a.clone()).combine(None), Some(a.clone())); assert_eq!( - Some(a).combine(Some(b)), + a.combine(b), // The value from `a` takes precedence - Some(OrderMap::from_iter([ - (1, "a"), - (2, "a"), - (3, "a"), - (0, "b"), - (5, "b") - ])) + OrderMap::<_, _>::from_iter([(0, "b"), (5, "b"), (1, "a"), (2, "a"), (3, "a"),]) ); } } diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 7e900e5a978db..4ada3d8b5b8da 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -918,6 +918,10 @@ impl SrcOptions { )] #[serde(rename_all = "kebab-case", transparent)] pub struct Rules { + /// The rules with their severity. Entries coming later in the map take precedence over + /// earlier entries (e.g. a `all` selector earlier in the hash map will be overridden + /// by a specific rule selector coming after it but if `all` is the last selector, then it + /// overrides even specific rule codes). inner: OrderMap, RangedValue, BuildHasherDefault>, } From c6418d2fee2bf97f5b39422a177e6fed8ae5829b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 6 Mar 2026 11:01:27 +0000 Subject: [PATCH 226/261] conformance.py: stop escaping `|` characters (#23758) --- scripts/conformance.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/conformance.py b/scripts/conformance.py index a7b83a4e690d5..9629ef5144caf 100644 --- a/scripts/conformance.py +++ b/scripts/conformance.py @@ -184,8 +184,6 @@ class TyDiagnostic: def __post_init__(self) -> None: # Remove check name prefix from description self.description = self.description.replace(f"{self.check_name}: ", "") - # Escape pipe characters for GitHub markdown tables - self.description = self.description.replace("|", "\\|") def __str__(self) -> str: return ( From 9b372a5ce0074680d67671d6429571d2ef2a767f Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Fri, 6 Mar 2026 11:11:04 +0000 Subject: [PATCH 227/261] installers: Download releases from Astral's mirror first (#23617) --- dist-workspace.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dist-workspace.toml b/dist-workspace.toml index 62da0f95d1e48..0f7f65a958689 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -67,6 +67,9 @@ github-custom-job-permissions = { "build-docker" = { packages = "write", content install-updater = false # Path that installers should place binaries in install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"] +# Prefer simple hosting for downloads, falling back to GitHub releases +hosting = ["simple", "github"] +simple-download-url = "https://releases.astral.sh/github/ruff/releases/download/{tag}" [dist.github-custom-runners] global = "depot-ubuntu-latest-4" From d9daf6d430623a8a5dd25cb52a3b6f2af961b9cb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 6 Mar 2026 11:15:43 +0000 Subject: [PATCH 228/261] [ty] Detect invalid partially stringified PEP-604 unions (#23285) --- crates/ty_ide/src/goto_type_definition.rs | 42 +-- crates/ty_ide/src/inlay_hints.rs | 42 +-- .../resources/mdtest/annotations/invalid.md | 8 +- .../resources/mdtest/annotations/string.md | 142 ++++++++- .../resources/mdtest/cycle.md | 2 +- .../resources/mdtest/implicit_type_aliases.md | 8 +- .../resources/mdtest/named_tuple.md | 2 +- .../resources/mdtest/narrow/isinstance.md | 2 + .../resources/mdtest/overloads.md | 2 + .../resources/mdtest/pep613_type_aliases.md | 4 +- .../resources/mdtest/pep695_type_aliases.md | 29 +- ...tringified_values_(5d8e1185129f8ae4).snap" | 46 +++ ...n_3.1\342\200\246_(5e6477d05ddea33f).snap" | 279 ++++++++++++++++++ .../mdtest/type_properties/is_subtype_of.md | 2 +- .../resources/mdtest/typed_dict.md | 4 +- .../types/infer/builder/type_expression.rs | 143 ++++++++- .../src/types/special_form.rs | 5 +- .../ty_extensions/ty_extensions.pyi | 6 +- 18 files changed, 700 insertions(+), 68 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/pep695_type_aliases.\342\200\246_-_PEP_695_type_aliases_-_Stringified_values_(5d8e1185129f8ae4).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index f1688037a6fc4..174476828db93 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -181,10 +181,10 @@ mod tests { --> stdlib/ty_extensions.pyi:15:1 | 13 | # Types - 14 | Unknown = object() - 15 | AlwaysTruthy = object() + 14 | Unknown: _SpecialForm + 15 | AlwaysTruthy: _SpecialForm | ------------ - 16 | AlwaysFalsy = object() + 16 | AlwaysFalsy: _SpecialForm | "); } @@ -939,10 +939,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ------- - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | "#); } @@ -1003,10 +1003,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ------- - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | "#); } @@ -1030,10 +1030,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ------- - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | "#); } @@ -1057,10 +1057,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ------- - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | "#); } @@ -1249,10 +1249,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ------- - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | "#); } @@ -2042,10 +2042,10 @@ def function(): --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ------- - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | "); } diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 87e40c7537a20..126d4bbcf0fce 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -2324,10 +2324,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | info: Source --> main2.py:5:18 @@ -2664,7 +2664,7 @@ mod tests { "#, ); - assert_snapshot!(test.inlay_hints(), @r###" + assert_snapshot!(test.inlay_hints(), @r#" a[: list[int]] = [1, 2] b[: list[int | float]] = [1.0, 2.0] @@ -3164,7 +3164,7 @@ mod tests { i: list[bytes] = [b'/x01', b'/x02'] j: list[int | float] = [+1, +2.0] k: list[int | float] = [-1, -2.0] - "###); + "#); } #[test] @@ -3349,7 +3349,7 @@ mod tests { "#, ); - assert_snapshot!(test.inlay_hints(), @r###" + assert_snapshot!(test.inlay_hints(), @r#" class MyClass[T, U]: def __init__(self, x: list[T], y: tuple[U, U]): @@ -4083,7 +4083,7 @@ mod tests { y: tuple[MyClass[int, str], MyClass[int, str]] = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) - "###); + "#); } #[test] @@ -4387,7 +4387,7 @@ mod tests { foo(y[0])", ); - assert_snapshot!(test.inlay_hints(), @r###" + assert_snapshot!(test.inlay_hints(), @r#" def foo(x: int): pass x[: list[int]] = [1] @@ -4496,7 +4496,7 @@ mod tests { foo(x[0]) foo(y[0]) - "###); + "#); } #[test] @@ -5525,10 +5525,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | info: Source --> main2.py:2:14 @@ -5543,10 +5543,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | info: Source --> main2.py:3:17 @@ -6530,10 +6530,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | info: Source --> main2.py:4:63 @@ -7260,10 +7260,10 @@ mod tests { --> stdlib/ty_extensions.pyi:14:1 | 13 | # Types - 14 | Unknown = object() + 14 | Unknown: _SpecialForm | ^^^^^^^ - 15 | AlwaysTruthy = object() - 16 | AlwaysFalsy = object() + 15 | AlwaysTruthy: _SpecialForm + 16 | AlwaysFalsy: _SpecialForm | info: Source --> main2.py:4:22 diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index d496ee0186475..5314e79db4b07 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -86,7 +86,9 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await` b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions" c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions" d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression" - e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" + # error: [unsupported-operator] + # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" + e: int | b"foo", f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions" @@ -97,7 +99,9 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await` m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" n: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions" o: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions" - p: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions" + # error: [unsupported-operator] + # error: [invalid-type-form] "F-strings are not allowed in type expressions" + p: int | f"foo", # error: [invalid-type-form] "Slices are not allowed in type expressions" # error: [invalid-type-form] "Invalid subscript" q: [1, 2, 3][1:2], diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index 77131c310a98a..ffa1df89bef23 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -35,7 +35,13 @@ def f(v: tuple[int, "str"]): def f(v: "Foo"): reveal_type(v) # revealed: Foo +def f(x: "int | 'Foo'"): ... + class Foo: ... + +f("not an int or a Foo") # error: [invalid-argument-type] +f(Foo()) # fine +f(42) # fine ``` ## Deferred (undefined) @@ -46,13 +52,145 @@ def f(v: "Foo"): reveal_type(v) # revealed: Unknown ``` -## Partial deferred +## Partially deferred annotations + +### Python less than 3.14 + +"Partially stringified" PEP-604 unions can raise `TypeError` on Python \<3.14; we try to detect this +common runtime error: + + + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import Any, TypeVar, Callable, Protocol, TypedDict, TYPE_CHECKING + +class TD(TypedDict): ... + +class P(Protocol): + x: int + +class Meta(type): + def __or__(cls, other: str) -> Any: + return "wow, so fancy, bet type checkers can't handle this" + +class UsesMeta(metaclass=Meta): ... + +T = TypeVar("T") + +# fmt: off +def f( + # error: [unsupported-operator] + a: int | "Foo", + # error: [unsupported-operator] + b: int | "memoryview" | bytes, + # error: [unsupported-operator] + c: "TD" | None, + # error: [unsupported-operator] + d: "P" | None, + # fine: `TypeVar.__or__` accepts strings at runtime + e: T | "Foo", + # fine: _SpecialForm.__ror__` accepts strings at runtime + f: "Foo" | Callable[..., None], + # also fine due to the custom metaclass + g: UsesMeta | "Foo", + # error: [unsupported-operator] + h: None | None, + # error: [unresolved-reference] "SomethingUndefined" + # error: [unresolved-reference] "SomethingAlsoUndefined" + i: SomethingUndefined | SomethingAlsoUndefined, +): + reveal_type(a) # revealed: int | Foo + reveal_type(b) # revealed: int | memoryview[int] | bytes + reveal_type(c) # revealed: TD | None + reveal_type(d) # revealed: P | None + reveal_type(e) # revealed: T@f | Foo + reveal_type(f) # revealed: Foo | ((...) -> None) + reveal_type(g) # revealed: UsesMeta | Foo + reveal_type(h) # revealed: None + reveal_type(i) # revealed: Unknown + +# fmt: on + +class Foo: ... + +# error: [unsupported-operator] +X = list["int" | None] + +if TYPE_CHECKING: + # TODO: ideally we would not error here, since `if TYPE_CHECKING` + # blocks are not executed at runtime. Requires + # https://github.com/astral-sh/ty/issues/1553. + bar: "int" | "None" # error: [unsupported-operator] + + # TODO: same as above + # error: [unsupported-operator] + def foo(x: "int" | "None"): ... + + class Bar: + # no error because this annotation is resolved inside a scope + # fully defined inside an `if TYPE_CHECKING` block + def f(x: "int" | "None"): ... +``` + +### Python less than 3.14 in a stub file + +This error is never emitted on stub files, because they are never executed at runtime: + +```toml +[environment] +python-version = "3.13" +``` + +```pyi +# fine +def f(x: "int" | None): ... +``` + +### Python less than 3.14 with `__future__` annotations + +The errors can be avoided in type-annotation contexts by using `__future__` annotations on Python +\<3.14: + +```toml +[environment] +python-version = "3.13" +``` ```py -def f(v: int | "Foo"): +from __future__ import annotations + +def f(v: int | "Foo"): # fine reveal_type(v) # revealed: int | Foo class Foo: ... + +# error: [unsupported-operator] +X = list["int" | None] +``` + +### Python >=3.14 + +Runtime errors are also less common for partially stringified annotations if the Python version +being used is >=3.14: + +```toml +[environment] +python-version = "3.14" +``` + +```py +def f(v: int | "Foo"): # fine + reveal_type(v) # revealed: int | Foo + +class Foo: ... + +# error: [unsupported-operator] +X = list["int" | None] ``` ## `typing.Literal` diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index d2aebe0f47734..4a7fc249c0345 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -42,7 +42,7 @@ python-version = "3.12" # typing.TypeAliasType ```py from typing import Union, TypeAliasType, Sequence, Mapping -A = list["A" | None] +A = list["A | None"] def f(x: A): # TODO: should be `list[A | None]`? diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 51eb51d0d8a54..858957625b3ca 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -1644,10 +1644,10 @@ python-version = "3.12" ```py from typing import List, Dict -RecursiveList1 = list["RecursiveList1" | None] -RecursiveList2 = List["RecursiveList2" | None] -RecursiveDict1 = dict[str, "RecursiveDict1" | None] -RecursiveDict2 = Dict[str, "RecursiveDict2" | None] +RecursiveList1 = list["RecursiveList1 | None"] +RecursiveList2 = List["RecursiveList2 | None"] +RecursiveDict1 = dict[str, "RecursiveDict1 | None"] +RecursiveDict2 = Dict[str, "RecursiveDict2 | None"] RecursiveDict3 = dict["RecursiveDict3", int] RecursiveDict4 = Dict["RecursiveDict4", int] diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index f9579ce8cbd71..83db0404acaca 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -246,7 +246,7 @@ Dangling calls cannot contain other dangling calls; that's an invalid type form: from ty_extensions import reveal_mro # error: [invalid-type-form] -class A(NamedTuple("B", [("x", NamedTuple("C", [("x", "A" | None)]))])): +class A(NamedTuple("B", [("x", NamedTuple("C", [("x", "A | None")]))])): pass # revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index d8c0e891dba98..b07d929a43d5d 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -174,6 +174,8 @@ python-version = "3.9" ``` ```py +from __future__ import annotations + def _(x: int | str | bytes): # error: [unsupported-operator] if isinstance(x, int | str): diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index 985288bd52222..c38d640f01b73 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -176,6 +176,8 @@ python-version = "3.9" ``` ```py +from __future__ import annotations + import sys from typing import overload diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index 8b1bb49e30d2d..d9879a852cf0c 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -310,13 +310,13 @@ def _(x: IntOrStr): from typing import TypeAlias, TypeVar, Union from types import UnionType -RecursiveTuple: TypeAlias = tuple[int | "RecursiveTuple", str] +RecursiveTuple: TypeAlias = tuple["int | RecursiveTuple", str] def _(rec: RecursiveTuple): # TODO should be `tuple[int | RecursiveTuple, str]` reveal_type(rec) # revealed: tuple[Divergent, str] -RecursiveHomogeneousTuple: TypeAlias = tuple[int | "RecursiveHomogeneousTuple", ...] +RecursiveHomogeneousTuple: TypeAlias = tuple["int | RecursiveHomogeneousTuple", ...] def _(rec: RecursiveHomogeneousTuple): # TODO should be `tuple[int | RecursiveHomogeneousTuple, ...]` diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index ac6b77d51dac7..a85b6c65c0716 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -131,6 +131,31 @@ def f(x: Foo[int]): reveal_type(x.foo()) # revealed: int ``` +## Stringified values + + + +Stringifying the right-hand side of a type alias is redundant, but allowed: + +```py +type X = "int | str" + +def f(obj: X): + reveal_type(obj) # revealed: int | str +``` + +The right-hand side of a PEP-695 type alias will not usually be executed, but can be if the user +accesses the `.__value__` attribute. Normal runtime rules still therefore apply regarding partially +stringified alias values: + +```py +# error: [unsupported-operator] +type Y = "int" | str + +def g(obj: Y): + reveal_type(obj) # revealed: int | str +``` + ## In unions and intersections We can "break apart" a type alias by e.g. adding it to a union: @@ -276,7 +301,7 @@ in a tuple unpacking is not supported. from typing_extensions import TypeAliasType # error: [invalid-type-alias-type] "A `TypeAliasType` definition must be a simple variable assignment" -TypeAliasType("IntOrStr", int | str) +TypeAliasType("IntOrStr", "int | str") ``` ### Mutually recursive `TypeAliasType` definitions @@ -469,7 +494,7 @@ def f(x: A): #### With new-style union ```py -type A = list["A" | str] +type A = list[A | str] def f(x: A): reveal_type(x) # revealed: list[A | str] diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/pep695_type_aliases.\342\200\246_-_PEP_695_type_aliases_-_Stringified_values_(5d8e1185129f8ae4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/pep695_type_aliases.\342\200\246_-_PEP_695_type_aliases_-_Stringified_values_(5d8e1185129f8ae4).snap" new file mode 100644 index 0000000000000..68965324829ac --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/pep695_type_aliases.\342\200\246_-_PEP_695_type_aliases_-_Stringified_values_(5d8e1185129f8ae4).snap" @@ -0,0 +1,46 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: pep695_type_aliases.md - PEP 695 type aliases - Stringified values +mdtest path: crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | type X = "int | str" +2 | +3 | def f(obj: X): +4 | reveal_type(obj) # revealed: int | str +5 | # error: [unsupported-operator] +6 | type Y = "int" | str +7 | +8 | def g(obj: Y): +9 | reveal_type(obj) # revealed: int | str +``` + +# Diagnostics + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:6:10 + | +4 | reveal_type(obj) # revealed: int | str +5 | # error: [unsupported-operator] +6 | type Y = "int" | str + | -----^^^--- + | | | + | | Has type `` + | Has type `Literal["int"]` +7 | +8 | def g(obj: Y): + | +info: A type alias scope is lazy but will be executed at runtime if the `__value__` property is accessed +info: rule `unsupported-operator` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" new file mode 100644 index 0000000000000..bb45195da772e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" @@ -0,0 +1,279 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: string.md - String annotations - Partially deferred annotations - Python less than 3.14 +mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/string.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Any, TypeVar, Callable, Protocol, TypedDict, TYPE_CHECKING + 2 | + 3 | class TD(TypedDict): ... + 4 | + 5 | class P(Protocol): + 6 | x: int + 7 | + 8 | class Meta(type): + 9 | def __or__(cls, other: str) -> Any: +10 | return "wow, so fancy, bet type checkers can't handle this" +11 | +12 | class UsesMeta(metaclass=Meta): ... +13 | +14 | T = TypeVar("T") +15 | +16 | # fmt: off +17 | def f( +18 | # error: [unsupported-operator] +19 | a: int | "Foo", +20 | # error: [unsupported-operator] +21 | b: int | "memoryview" | bytes, +22 | # error: [unsupported-operator] +23 | c: "TD" | None, +24 | # error: [unsupported-operator] +25 | d: "P" | None, +26 | # fine: `TypeVar.__or__` accepts strings at runtime +27 | e: T | "Foo", +28 | # fine: _SpecialForm.__ror__` accepts strings at runtime +29 | f: "Foo" | Callable[..., None], +30 | # also fine due to the custom metaclass +31 | g: UsesMeta | "Foo", +32 | # error: [unsupported-operator] +33 | h: None | None, +34 | # error: [unresolved-reference] "SomethingUndefined" +35 | # error: [unresolved-reference] "SomethingAlsoUndefined" +36 | i: SomethingUndefined | SomethingAlsoUndefined, +37 | ): +38 | reveal_type(a) # revealed: int | Foo +39 | reveal_type(b) # revealed: int | memoryview[int] | bytes +40 | reveal_type(c) # revealed: TD | None +41 | reveal_type(d) # revealed: P | None +42 | reveal_type(e) # revealed: T@f | Foo +43 | reveal_type(f) # revealed: Foo | ((...) -> None) +44 | reveal_type(g) # revealed: UsesMeta | Foo +45 | reveal_type(h) # revealed: None +46 | reveal_type(i) # revealed: Unknown +47 | +48 | # fmt: on +49 | +50 | class Foo: ... +51 | +52 | # error: [unsupported-operator] +53 | X = list["int" | None] +54 | +55 | if TYPE_CHECKING: +56 | # TODO: ideally we would not error here, since `if TYPE_CHECKING` +57 | # blocks are not executed at runtime. Requires +58 | # https://github.com/astral-sh/ty/issues/1553. +59 | bar: "int" | "None" # error: [unsupported-operator] +60 | +61 | # TODO: same as above +62 | # error: [unsupported-operator] +63 | def foo(x: "int" | "None"): ... +64 | +65 | class Bar: +66 | # no error because this annotation is resolved inside a scope +67 | # fully defined inside an `if TYPE_CHECKING` block +68 | def f(x: "int" | "None"): ... +``` + +# Diagnostics + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:19:8 + | +17 | def f( +18 | # error: [unsupported-operator] +19 | a: int | "Foo", + | ---^^^----- + | | | + | | Has type `Literal["Foo"]` + | Has type `` +20 | # error: [unsupported-operator] +21 | b: int | "memoryview" | bytes, + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:21:8 + | +19 | a: int | "Foo", +20 | # error: [unsupported-operator] +21 | b: int | "memoryview" | bytes, + | ---^^^------------ + | | | + | | Has type `Literal["memoryview"]` + | Has type `` +22 | # error: [unsupported-operator] +23 | c: "TD" | None, + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:23:8 + | +21 | b: int | "memoryview" | bytes, +22 | # error: [unsupported-operator] +23 | c: "TD" | None, + | ----^^^---- + | | | + | | Has type `None` + | Has type `Literal["TD"]` +24 | # error: [unsupported-operator] +25 | d: "P" | None, + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:25:8 + | +23 | c: "TD" | None, +24 | # error: [unsupported-operator] +25 | d: "P" | None, + | ---^^^---- + | | | + | | Has type `None` + | Has type `Literal["P"]` +26 | # fine: `TypeVar.__or__` accepts strings at runtime +27 | e: T | "Foo", + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:33:8 + | +31 | g: UsesMeta | "Foo", +32 | # error: [unsupported-operator] +33 | h: None | None, + | ^^^^^^^^^^^ Both operands have type `None` +34 | # error: [unresolved-reference] "SomethingUndefined" +35 | # error: [unresolved-reference] "SomethingAlsoUndefined" + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `SomethingUndefined` used when not defined + --> src/mdtest_snippet.py:36:8 + | +34 | # error: [unresolved-reference] "SomethingUndefined" +35 | # error: [unresolved-reference] "SomethingAlsoUndefined" +36 | i: SomethingUndefined | SomethingAlsoUndefined, + | ^^^^^^^^^^^^^^^^^^ +37 | ): +38 | reveal_type(a) # revealed: int | Foo + | +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `SomethingAlsoUndefined` used when not defined + --> src/mdtest_snippet.py:36:29 + | +34 | # error: [unresolved-reference] "SomethingUndefined" +35 | # error: [unresolved-reference] "SomethingAlsoUndefined" +36 | i: SomethingUndefined | SomethingAlsoUndefined, + | ^^^^^^^^^^^^^^^^^^^^^^ +37 | ): +38 | reveal_type(a) # revealed: int | Foo + | +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:53:10 + | +52 | # error: [unsupported-operator] +53 | X = list["int" | None] + | -----^^^---- + | | | + | | Has type `None` + | Has type `Literal["int"]` +54 | +55 | if TYPE_CHECKING: + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:59:10 + | +57 | # blocks are not executed at runtime. Requires +58 | # https://github.com/astral-sh/ty/issues/1553. +59 | bar: "int" | "None" # error: [unsupported-operator] + | -----^^^------ + | | | + | | Has type `Literal["None"]` + | Has type `Literal["int"]` +60 | +61 | # TODO: same as above + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:63:16 + | +61 | # TODO: same as above +62 | # error: [unsupported-operator] +63 | def foo(x: "int" | "None"): ... + | -----^^^------ + | | | + | | Has type `Literal["None"]` + | Has type `Literal["int"]` +64 | +65 | class Bar: + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 70e4d2e57a105..ccf8c918a33f7 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -2025,7 +2025,7 @@ from dataclasses import dataclass @dataclass class A: - x: "A" | None + x: "A | None" static_assert(is_subtype_of(type[A], Callable[[A], A])) static_assert(is_subtype_of(type[A], Callable[[None], A])) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index c9d892e2e45df..e9dc45f424fa7 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -1277,11 +1277,11 @@ from ty_extensions import static_assert, is_assignable_to, is_equivalent_to class Node1(TypedDict): value: int - next: "Node1" | None + next: "Node1 | None" class Node2(TypedDict): value: int - next: "Node2" | None + next: "Node2 | None" static_assert(is_assignable_to(Node1, Node2)) static_assert(is_equivalent_to(Node1, Node2)) diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 56e882c7c0b33..d5f653d06686a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1,13 +1,14 @@ use itertools::Either; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast, PythonVersion}; use super::{DeferredExpressionState, TypeInferenceBuilder}; -use crate::FxOrderSet; +use crate::semantic_index::scope::ScopeKind; use crate::types::diagnostic::{ - self, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, UNBOUND_TYPE_VARIABLE, + self, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, UNBOUND_TYPE_VARIABLE, UNSUPPORTED_OPERATOR, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable, }; -use crate::types::infer::builder::{InferenceFlags, InnerExpressionInferenceState}; +use crate::types::infer::InferenceFlags; +use crate::types::infer::builder::{InnerExpressionInferenceState, MultiInferenceState}; use crate::types::signatures::Signature; use crate::types::special_form::{AliasSpec, LegacyStdlibAlias}; use crate::types::string_annotation::parse_string_annotation; @@ -18,6 +19,7 @@ use crate::types::{ SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeContext, TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, any_over_type, todo_type, }; +use crate::{FxOrderSet, Program, add_inferred_python_version_hint_to_diagnostic}; /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { @@ -153,6 +155,139 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Operator::BitOr => { let left_ty = self.infer_type_expression(&binary.left); let right_ty = self.infer_type_expression(&binary.right); + + // Detect runtime errors from e.g. `int | "bytes"` on Python <3.14 without `__future__` annotations. + if !self.deferred_state.is_deferred() + && !self.scope.scope(self.db()).in_type_checking_block() + { + let previous_state = + self.set_multi_inference_state(MultiInferenceState::Ignore); + self.context.set_multi_inference(true); + // If the left-hand side of the union is itself a PEP-604 union, + // we'll already have checked whether it can be used with `|` in a previous inference step + // and emitted a diagnostic if it was appropriate. We should skip inferring it here to + // avoid duplicate diagnostics; just assume that the l.h.s. is a `UnionType` instance + // in that case. + let left_type_value = + self.infer_expression(&binary.left, TypeContext::default()); + let right_type_value = + self.infer_expression(&binary.right, TypeContext::default()); + self.multi_inference_state = previous_state; + self.context.set_multi_inference(false); + + let dunder_fails = Type::try_call_bin_op( + self.db(), + left_type_value, + ast::Operator::BitOr, + right_type_value, + ) + .is_err(); + + // As well as trying the normal dunder lookup, + // we also check for the case where one of the operands is a class-literal type + // and the other is a string literal. The normal dunder lookup fails to catch + // this error, since typeshed annotates `type.__(r)or__` as accepting `Any`. + let should_emit_error = dunder_fails + || matches!( + (left_type_value, right_type_value), + ( + Type::ClassLiteral(class), Type::LiteralValue(literal)) + | (Type::LiteralValue(literal), Type::ClassLiteral(class) + ) + if class.metaclass(self.db()) == KnownClass::Type.to_class_literal(self.db()) + && !literal.is_enum() + ); + + if should_emit_error + && let Some(builder) = + self.context.report_lint(&UNSUPPORTED_OPERATOR, binary) + { + let mut diagnostic = + builder.into_diagnostic("Unsupported `|` operation"); + + if left_type_value.is_equivalent_to(self.db(), right_type_value) { + diagnostic.set_primary_message(format_args!( + "Both operands have type `{}`", + left_type_value.display(self.db()) + )); + diagnostic.set_concise_message(format_args!( + "Operator `|` is unsupported between \ + two objects of type `{}`", + left_type_value.display(self.db()) + )); + } else { + for (operand, ty) in [ + (&*binary.left, left_type_value), + (&*binary.right, right_type_value), + ] { + diagnostic.annotate( + self.context.secondary(operand).message(format_args!( + "Has type `{}`", + ty.display(self.db()) + )), + ); + } + diagnostic.set_concise_message(format_args!( + "Operator `|` is unsupported between \ + objects of type `{}` and `{}`", + left_type_value.display(self.db()), + right_type_value.display(self.db()) + )); + } + + match self.scope.scope(self.db()).kind() { + ScopeKind::TypeAlias => diagnostic.info( + "A type alias scope is lazy but will be \ + executed at runtime if the `__value__` property is \ + accessed", + ), + ScopeKind::TypeParams => diagnostic.info( + "Type parameter scopes are lazy but may be \ + executed at runtime if the `__bound__`, `__value__` + or `__constraints__` property of a type parameter is \ + accessed", + ), + _ => { + let python_version = + Program::get(self.db()).python_version(self.db()); + + if python_version < PythonVersion::PY310 + && !binary.left.is_string_literal_expr() + && !binary.right.is_string_literal_expr() + { + diagnostic.info( + "PEP 604 `|` unions are only available on \ + Python 3.10+ unless they are quoted", + ); + add_inferred_python_version_hint_to_diagnostic( + self.db(), + &mut diagnostic, + "inferring types", + ); + } else if python_version < PythonVersion::PY314 { + diagnostic.info( + "All type expressions are evaluated at \ + runtime by default on Python <3.14", + ); + add_inferred_python_version_hint_to_diagnostic( + self.db(), + &mut diagnostic, + "inferring types", + ); + if binary.left.is_string_literal_expr() + || binary.right.is_string_literal_expr() + { + diagnostic.help( + "Put quotes around the whole union \ + rather than just certain elements", + ); + } + } + } + } + } + } + UnionType::from_elements_leave_aliases(self.db(), [left_ty, right_ty]) } // anything else is an invalid annotation: diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 96fc2f2dceff6..7a0966c3ac4c7 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -143,6 +143,9 @@ impl SpecialFormType { | Self::Bottom | Self::Intersection | Self::CallableTypeOf + | Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy | Self::TypeQualifier(_) => KnownClass::SpecialForm, // Typeshed says it's an instance of `_SpecialForm`, @@ -154,8 +157,6 @@ impl SpecialFormType { Self::LegacyStdlibAlias(_) => KnownClass::StdlibAlias, - Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy => KnownClass::Object, - Self::NamedTuple => KnownClass::FunctionType, } } diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 1fb55e591dad9..9d6313744e327 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -11,9 +11,9 @@ from typing_extensions import LiteralString, Self # noqa: UP035 def static_assert(condition: object, msg: LiteralString | None = None) -> None: ... # Types -Unknown = object() -AlwaysTruthy = object() -AlwaysFalsy = object() +Unknown: _SpecialForm +AlwaysTruthy: _SpecialForm +AlwaysFalsy: _SpecialForm # Special forms Not: _SpecialForm From 8a10b6743c1aedc56c44899edacbe0d3341db927 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 6 Mar 2026 09:12:21 -0500 Subject: [PATCH 229/261] Add support for `lazy` import parsing (#23755) ## Summary This PR adds `is_lazy` to the AST and parser, and supports `lazy` import formatting in the formatter. The name `is_lazy` matches the [CPython AST](https://github.com/python/cpython/blob/c3fb0d9d96902774c08b199dda0479a8d31398a5/Parser/Python.asdl#L48), though we could modify it if there's demand. There are no further changes to Ruff rules or even to Ruff import sorting, since those deserve separate design discussions. I've also omitted support for flagging semantic syntax errors (e.g., `lazy import` inside `def`). See: https://github.com/astral-sh/ruff/issues/21305. --- crates/ruff_graph/src/collector.rs | 2 + .../src/checkers/ast/analyze/statement.rs | 2 + crates/ruff_linter/src/checkers/ast/mod.rs | 2 + crates/ruff_linter/src/importer/mod.rs | 1 + .../rules/relative_imports.rs | 3 +- .../ruff_linter/src/rules/isort/annotate.rs | 2 + .../rules/isort/rules/add_required_imports.rs | 2 + .../rules/pylint/rules/manual_import_from.rs | 4 + .../rules/deprecated_c_element_tree.rs | 2 + .../pyupgrade/rules/deprecated_mock_import.rs | 1 + crates/ruff_python_ast/ast.toml | 6 +- crates/ruff_python_ast/src/comparable.rs | 6 + crates/ruff_python_ast/src/generated.rs | 4 + crates/ruff_python_ast/src/token.rs | 2 + crates/ruff_python_ast/src/visitor.rs | 1 + .../src/visitor/transformer.rs | 1 + crates/ruff_python_codegen/src/generator.rs | 8 + .../ruff/statement/lazy_import.options.json | 5 + .../fixtures/ruff/statement/lazy_import.py | 12 ++ .../src/statement/stmt_import.rs | 4 + .../src/statement/stmt_import_from.rs | 5 + .../format@statement__lazy_import.py.snap | 53 +++++ .../inline/err/lazy_import_stmt_py314.py | 3 + .../from_import_soft_keyword_module_name.py | 1 + .../inline/ok/import_as_name_soft_keyword.py | 1 + .../inline/ok/lazy_import_relative_py315.py | 4 + .../lazy_import_soft_keyword_split_py315.py | 6 + .../inline/ok/lazy_import_stmt_py315.py | 8 + crates/ruff_python_parser/src/error.rs | 3 + crates/ruff_python_parser/src/lexer.rs | 1 + .../src/parser/statement.rs | 79 ++++++- ...invalid_syntax@debug_shadow_import.py.snap | 5 +- ...d_syntax@dotted_name_multiple_dots.py.snap | 3 +- ...id_syntax@from_import_dotted_names.py.snap | 4 +- ...lid_syntax@from_import_empty_names.py.snap | 4 +- ..._syntax@from_import_missing_module.py.snap | 3 +- ...id_syntax@from_import_missing_rpar.py.snap | 3 +- ...@from_import_star_with_other_names.py.snap | 5 +- ...ort_unparenthesized_trailing_comma.py.snap | 4 +- ...syntax@import_alias_missing_asname.py.snap | 2 +- .../invalid_syntax@import_from_star.py.snap | 5 +- .../invalid_syntax@import_stmt_empty.py.snap | 2 +- ...ax@import_stmt_parenthesized_names.py.snap | 3 +- ...lid_syntax@import_stmt_star_import.py.snap | 3 +- ..._syntax@import_stmt_trailing_comma.py.snap | 3 +- ...alid_syntax@invalid_future_feature.py.snap | 4 +- ...alid_syntax@lazy_import_stmt_py314.py.snap | 77 +++++++ .../valid_syntax@debug_rename_import.py.snap | 4 +- ...ntax@dotted_name_normalized_spaces.py.snap | 3 +- .../valid_syntax@from_import_no_space.py.snap | 3 +- ...om_import_soft_keyword_module_name.py.snap | 42 +++- ...syntax@from_import_stmt_terminator.py.snap | 5 +- ...syntax@import_as_name_soft_keyword.py.snap | 31 ++- .../valid_syntax@import_from_star.py.snap | 2 +- ...alid_syntax@import_stmt_terminator.py.snap | 4 +- ..._syntax@lazy_import_relative_py315.py.snap | 104 ++++++++++ ...zy_import_soft_keyword_split_py315.py.snap | 89 ++++++++ ...alid_syntax@lazy_import_stmt_py315.py.snap | 196 ++++++++++++++++++ ...x@param_with_star_annotation_py310.py.snap | 2 +- ...yntax@simple_stmts_with_semicolons.py.snap | 3 +- ...alid_syntax@statement__from_import.py.snap | 10 +- .../valid_syntax@statement__import.py.snap | 6 +- .../valid_syntax@valid_future_feature.py.snap | 2 +- crates/ruff_python_semantic/src/imports.rs | 2 + crates/ruff_python_trivia/src/tokenizer.rs | 4 + crates/ty_module_resolver/src/module_name.rs | 1 + .../src/types/infer/builder.rs | 8 +- 67 files changed, 839 insertions(+), 46 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/lazy_import.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/lazy_import.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__lazy_import.py.snap create mode 100644 crates/ruff_python_parser/resources/inline/err/lazy_import_stmt_py314.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/lazy_import_relative_py315.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/lazy_import_soft_keyword_split_py315.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/lazy_import_stmt_py315.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_relative_py315.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_soft_keyword_split_py315.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_stmt_py315.py.snap diff --git a/crates/ruff_graph/src/collector.rs b/crates/ruff_graph/src/collector.rs index 55e4937bdaf7a..7d01d9fd7cdc8 100644 --- a/crates/ruff_graph/src/collector.rs +++ b/crates/ruff_graph/src/collector.rs @@ -46,6 +46,7 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> { names, module, level, + is_lazy: _, range: _, node_index: _, }) => { @@ -89,6 +90,7 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> { } Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) => { diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 51f2fa74f5383..47873d99ec4d7 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -532,6 +532,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) => { @@ -690,6 +691,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { names, module, level, + is_lazy: _, range: _, node_index: _, }, diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 475839030296a..93b66944e3d20 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1002,6 +1002,7 @@ impl<'a> Visitor<'a> for Checker<'a> { } Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) => { @@ -1057,6 +1058,7 @@ impl<'a> Visitor<'a> for Checker<'a> { names, module, level, + is_lazy: _, range: _, node_index: _, }) => { diff --git a/crates/ruff_linter/src/importer/mod.rs b/crates/ruff_linter/src/importer/mod.rs index 4ffa03d6774e0..4bd511844750c 100644 --- a/crates/ruff_linter/src/importer/mod.rs +++ b/crates/ruff_linter/src/importer/mod.rs @@ -457,6 +457,7 @@ impl<'a> Importer<'a> { module: name, names, level, + is_lazy: _, range: _, node_index: _, }) = stmt diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs index bc77a55716f4a..2704add61642d 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs @@ -91,7 +91,7 @@ fn fix_banned_relative_import( return None; } - let Stmt::ImportFrom(ast::StmtImportFrom { names, .. }) = stmt else { + let Stmt::ImportFrom(ast::StmtImportFrom { names, is_lazy, .. }) = stmt else { panic!("Expected Stmt::ImportFrom"); }; let node = ast::StmtImportFrom { @@ -101,6 +101,7 @@ fn fix_banned_relative_import( )), names: names.clone(), level: 0, + is_lazy: *is_lazy, range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; diff --git a/crates/ruff_linter/src/rules/isort/annotate.rs b/crates/ruff_linter/src/rules/isort/annotate.rs index 585b58e65151b..122781898a7c9 100644 --- a/crates/ruff_linter/src/rules/isort/annotate.rs +++ b/crates/ruff_linter/src/rules/isort/annotate.rs @@ -26,6 +26,7 @@ pub(crate) fn annotate_imports<'a>( Stmt::Import(ast::StmtImport { names, range, + is_lazy: _, node_index: _, }) => { // Find comments above. @@ -62,6 +63,7 @@ pub(crate) fn annotate_imports<'a>( module, names, level, + is_lazy: _, range: _, node_index: _, }) => { diff --git a/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs b/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs index d388fcfe7646b..0f2146d5cc78b 100644 --- a/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs @@ -61,6 +61,7 @@ fn includes_import(stmt: &Stmt, target: &NameImport) -> bool { NameImport::Import(target) => { let Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) = &stmt @@ -77,6 +78,7 @@ fn includes_import(stmt: &Stmt, target: &NameImport) -> bool { module, names, level, + is_lazy: _, range: _, node_index: _, }) = &stmt diff --git a/crates/ruff_linter/src/rules/pylint/rules/manual_import_from.rs b/crates/ruff_linter/src/rules/pylint/rules/manual_import_from.rs index 4a4af16837241..4fe905a500638 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/manual_import_from.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/manual_import_from.rs @@ -83,6 +83,9 @@ pub(crate) fn manual_from_import(checker: &Checker, stmt: &Stmt, alias: &Alias, alias.range(), ); if names.len() == 1 { + let is_lazy = stmt + .as_import_stmt() + .is_some_and(|import_stmt| import_stmt.is_lazy); let node = ast::StmtImportFrom { module: Some(Identifier::new(module.to_string(), TextRange::default())), names: vec![Alias { @@ -92,6 +95,7 @@ pub(crate) fn manual_from_import(checker: &Checker, stmt: &Stmt, alias: &Alias, node_index: ruff_python_ast::AtomicNodeIndex::NONE, }], level: 0, + is_lazy, range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs index 1ce34e2f153ae..34ac95f9e9c54 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs @@ -57,6 +57,7 @@ pub(crate) fn deprecated_c_element_tree(checker: &Checker, stmt: &Stmt) { match stmt { Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) => { @@ -71,6 +72,7 @@ pub(crate) fn deprecated_c_element_tree(checker: &Checker, stmt: &Stmt) { module, names, level, + is_lazy: _, range: _, node_index: _, }) => { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs index 262a825eedd60..fe5638267f1c5 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -285,6 +285,7 @@ pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) { match stmt { Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) => { diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index cbcde4e21332c..b046d3731ae9e 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -213,7 +213,10 @@ fields = [{ name = "test", type = "Expr" }, { name = "msg", type = "Expr?" }] [Stmt.nodes.StmtImport] doc = "See also [Import](https://docs.python.org/3/library/ast.html#ast.Import)" -fields = [{ name = "names", type = "Alias*" }] +fields = [ + { name = "names", type = "Alias*" }, + { name = "is_lazy", type = "bool" }, +] [Stmt.nodes.StmtImportFrom] doc = "See also [ImportFrom](https://docs.python.org/3/library/ast.html#ast.ImportFrom)" @@ -221,6 +224,7 @@ fields = [ { name = "module", type = "Identifier?" }, { name = "names", type = "Alias*" }, { name = "level", type = "u32" }, + { name = "is_lazy", type = "bool" }, ] [Stmt.nodes.StmtGlobal] diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index c9ba3ac39860f..8ae6b22d9eca8 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -1532,6 +1532,7 @@ pub struct StmtAssert<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct StmtImport<'a> { names: Vec>, + is_lazy: bool, } #[derive(Debug, PartialEq, Eq, Hash)] @@ -1539,6 +1540,7 @@ pub struct StmtImportFrom<'a> { module: Option<&'a str>, names: Vec>, level: u32, + is_lazy: bool, } #[derive(Debug, PartialEq, Eq, Hash)] @@ -1778,21 +1780,25 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { }), ast::Stmt::Import(ast::StmtImport { names, + is_lazy, range: _, node_index: _, }) => Self::Import(StmtImport { names: names.iter().map(Into::into).collect(), + is_lazy: *is_lazy, }), ast::Stmt::ImportFrom(ast::StmtImportFrom { module, names, level, + is_lazy, range: _, node_index: _, }) => Self::ImportFrom(StmtImportFrom { module: module.as_deref(), names: names.iter().map(Into::into).collect(), level: *level, + is_lazy: *is_lazy, }), ast::Stmt::Global(ast::StmtGlobal { names, diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index 547c50d631962..8af0840188906 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -9164,6 +9164,7 @@ pub struct StmtImport { pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub names: Vec, + pub is_lazy: bool, } /// See also [ImportFrom](https://docs.python.org/3/library/ast.html#ast.ImportFrom) @@ -9175,6 +9176,7 @@ pub struct StmtImportFrom { pub module: Option, pub names: Vec, pub level: u32, + pub is_lazy: bool, } /// See also [Global](https://docs.python.org/3/library/ast.html#ast.Global) @@ -10126,6 +10128,7 @@ impl StmtImport { { let StmtImport { names, + is_lazy: _, range: _, node_index: _, } = self; @@ -10145,6 +10148,7 @@ impl StmtImportFrom { module, names, level: _, + is_lazy: _, range: _, node_index: _, } = self; diff --git a/crates/ruff_python_ast/src/token.rs b/crates/ruff_python_ast/src/token.rs index 4b9d98ec5c910..fcfd42642d354 100644 --- a/crates/ruff_python_ast/src/token.rs +++ b/crates/ruff_python_ast/src/token.rs @@ -314,6 +314,7 @@ pub enum TokenKind { // Soft keywords Case, + Lazy, Match, Type, @@ -724,6 +725,7 @@ impl fmt::Display for TokenKind { TokenKind::Return => "`return`", TokenKind::Try => "`try`", TokenKind::While => "`while`", + TokenKind::Lazy => "`lazy`", TokenKind::Match => "`match`", TokenKind::Type => "`type`", TokenKind::Case => "`case`", diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 425c317dc36fd..80d025385c70b 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -330,6 +330,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { } Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) => { diff --git a/crates/ruff_python_ast/src/visitor/transformer.rs b/crates/ruff_python_ast/src/visitor/transformer.rs index 3a526c04713a6..b999ab18d521f 100644 --- a/crates/ruff_python_ast/src/visitor/transformer.rs +++ b/crates/ruff_python_ast/src/visitor/transformer.rs @@ -314,6 +314,7 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { } Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) => { diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index 362d00d235ec7..c03975348a1db 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -634,10 +634,14 @@ impl<'a> Generator<'a> { } Stmt::Import(ast::StmtImport { names, + is_lazy, range: _, node_index: _, }) => { statement!({ + if *is_lazy { + self.p("lazy "); + } self.p("import "); let mut first = true; for alias in names { @@ -650,10 +654,14 @@ impl<'a> Generator<'a> { module, names, level, + is_lazy, range: _, node_index: _, }) => { statement!({ + if *is_lazy { + self.p("lazy "); + } self.p("from "); if *level > 0 { for _ in 0..*level { diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/lazy_import.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/lazy_import.options.json new file mode 100644 index 0000000000000..c5893b5e1a839 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/lazy_import.options.json @@ -0,0 +1,5 @@ +[ + { + "target_version": "3.15" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/lazy_import.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/lazy_import.py new file mode 100644 index 0000000000000..4022e95cede0d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/lazy_import.py @@ -0,0 +1,12 @@ +lazy import foo +lazy import foo, bar as baz + +lazy from a import b +lazy from some.really.long.module.name import first_really_long_name, second_really_long_name as renamed_value +lazy from a import ( # comment + bar, +) + +def f(): + lazy import foo.bar + lazy from another.really.long.module.path import first_name, second_name as alias diff --git a/crates/ruff_python_formatter/src/statement/stmt_import.rs b/crates/ruff_python_formatter/src/statement/stmt_import.rs index f922fd0b980a9..35f15307a5441 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import.rs @@ -10,6 +10,7 @@ impl FormatNodeRule for FormatStmtImport { fn fmt_fields(&self, item: &StmtImport, f: &mut PyFormatter) -> FormatResult<()> { let StmtImport { names, + is_lazy, range: _, node_index: _, } = item; @@ -18,6 +19,9 @@ impl FormatNodeRule for FormatStmtImport { .entries(names.iter().formatted()) .finish() }); + if *is_lazy { + write!(f, [token("lazy"), space()])?; + } write!(f, [token("import"), space(), names]) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs index 4a8d92f545e7b..6b715eab830dd 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs @@ -16,10 +16,15 @@ impl FormatNodeRule for FormatStmtImportFrom { module, names, level, + is_lazy, range: _, node_index: _, } = item; + if *is_lazy { + write!(f, [token("lazy"), space()])?; + } + write!( f, [ diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__lazy_import.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__lazy_import.py.snap new file mode 100644 index 0000000000000..74cd81cf8d70e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__lazy_import.py.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +--- +## Input +```python +lazy import foo +lazy import foo, bar as baz + +lazy from a import b +lazy from some.really.long.module.name import first_really_long_name, second_really_long_name as renamed_value +lazy from a import ( # comment + bar, +) + +def f(): + lazy import foo.bar + lazy from another.really.long.module.path import first_name, second_name as alias +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.15 +source_type = Python +``` + +```python +lazy import foo +lazy import foo, bar as baz + +lazy from a import b +lazy from some.really.long.module.name import ( + first_really_long_name, + second_really_long_name as renamed_value, +) +lazy from a import ( # comment + bar, +) + + +def f(): + lazy import foo.bar + lazy from another.really.long.module.path import first_name, second_name as alias +``` diff --git a/crates/ruff_python_parser/resources/inline/err/lazy_import_stmt_py314.py b/crates/ruff_python_parser/resources/inline/err/lazy_import_stmt_py314.py new file mode 100644 index 0000000000000..ec7ba9c4d9f2a --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/lazy_import_stmt_py314.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +lazy import foo +lazy from bar import baz diff --git a/crates/ruff_python_parser/resources/inline/ok/from_import_soft_keyword_module_name.py b/crates/ruff_python_parser/resources/inline/ok/from_import_soft_keyword_module_name.py index fb617bd3f43d2..048164eaed6b6 100644 --- a/crates/ruff_python_parser/resources/inline/ok/from_import_soft_keyword_module_name.py +++ b/crates/ruff_python_parser/resources/inline/ok/from_import_soft_keyword_module_name.py @@ -1,4 +1,5 @@ from match import pattern from type import bar from case import pattern +from lazy import qux from match.type.case import foo diff --git a/crates/ruff_python_parser/resources/inline/ok/import_as_name_soft_keyword.py b/crates/ruff_python_parser/resources/inline/ok/import_as_name_soft_keyword.py index 5f68a60cd1b26..ef1883e55f3a0 100644 --- a/crates/ruff_python_parser/resources/inline/ok/import_as_name_soft_keyword.py +++ b/crates/ruff_python_parser/resources/inline/ok/import_as_name_soft_keyword.py @@ -1,3 +1,4 @@ import foo as match import bar as case import baz as type +import qux as lazy diff --git a/crates/ruff_python_parser/resources/inline/ok/lazy_import_relative_py315.py b/crates/ruff_python_parser/resources/inline/ok/lazy_import_relative_py315.py new file mode 100644 index 0000000000000..63afd60f332bc --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/lazy_import_relative_py315.py @@ -0,0 +1,4 @@ +# parse_options: {"target-version": "3.15"} +lazy from . import basic2 +lazy from .basic2 import x, f +lazy from . import b, x diff --git a/crates/ruff_python_parser/resources/inline/ok/lazy_import_soft_keyword_split_py315.py b/crates/ruff_python_parser/resources/inline/ok/lazy_import_soft_keyword_split_py315.py new file mode 100644 index 0000000000000..340999cf4ed45 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/lazy_import_soft_keyword_split_py315.py @@ -0,0 +1,6 @@ +# parse_options: {"target-version": "3.15"} +lazy +import os + +lazy # comment +from sys import path diff --git a/crates/ruff_python_parser/resources/inline/ok/lazy_import_stmt_py315.py b/crates/ruff_python_parser/resources/inline/ok/lazy_import_stmt_py315.py new file mode 100644 index 0000000000000..7fbb2ab75bf81 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/lazy_import_stmt_py315.py @@ -0,0 +1,8 @@ +# parse_options: {"target-version": "3.15"} +lazy import foo +lazy import foo as bar +lazy from bar import baz +lazy from sys import x as y +lazy = 1 +import foo as lazy +from lazy import qux diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 6dd1dac0d3643..151a53367b0ce 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -710,6 +710,7 @@ pub enum UnsupportedSyntaxErrorKind { /// [PEP 695]: https://peps.python.org/pep-0695/ /// [`typing.TypeVar`]: https://docs.python.org/3/library/typing.html#typevar TypeParameterList, + LazyImportStatement, TypeAliasStatement, TypeParamDefault, @@ -957,6 +958,7 @@ impl Display for UnsupportedSyntaxError { "Cannot use positional-only parameter separator" } UnsupportedSyntaxErrorKind::TypeParameterList => "Cannot use type parameter lists", + UnsupportedSyntaxErrorKind::LazyImportStatement => "Cannot use `lazy` import statement", UnsupportedSyntaxErrorKind::TypeAliasStatement => "Cannot use `type` alias statement", UnsupportedSyntaxErrorKind::TypeParamDefault => { "Cannot set default type for a type parameter" @@ -1038,6 +1040,7 @@ impl UnsupportedSyntaxErrorKind { Change::Removed(PythonVersion::PY38) } UnsupportedSyntaxErrorKind::TypeParameterList => Change::Added(PythonVersion::PY312), + UnsupportedSyntaxErrorKind::LazyImportStatement => Change::Added(PythonVersion::PY315), UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312), UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313), UnsupportedSyntaxErrorKind::Pep701FString(_) => Change::Added(PythonVersion::PY312), diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index 31f4342d75570..4c9ab6b464d05 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -724,6 +724,7 @@ impl<'src> Lexer<'src> { "import" => TokenKind::Import, "in" => TokenKind::In, "is" => TokenKind::Is, + "lazy" => TokenKind::Lazy, "lambda" => TokenKind::Lambda, "match" => TokenKind::Match, "nonlocal" => TokenKind::Nonlocal, diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 6f0ed73fbd712..b52d527b5e349 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -263,8 +263,14 @@ impl<'src> Parser<'src> { fn parse_simple_statement(&mut self) -> Stmt { match self.current_token_kind() { TokenKind::Return => Stmt::Return(self.parse_return_statement()), - TokenKind::Import => Stmt::Import(self.parse_import_statement()), - TokenKind::From => Stmt::ImportFrom(self.parse_from_import_statement()), + TokenKind::Import => { + let start = self.node_start(); + Stmt::Import(self.parse_import_statement(start, false)) + } + TokenKind::From => { + let start = self.node_start(); + Stmt::ImportFrom(self.parse_from_import_statement(start, false)) + } TokenKind::Pass => Stmt::Pass(self.parse_pass_statement()), TokenKind::Continue => Stmt::Continue(self.parse_continue_statement()), TokenKind::Break => Stmt::Break(self.parse_break_statement()), @@ -277,6 +283,59 @@ impl<'src> Parser<'src> { Stmt::IpyEscapeCommand(self.parse_ipython_escape_command_statement()) } token => { + if token == TokenKind::Lazy { + let start = self.node_start(); + let lazy_range = self.current_token_range(); + + match self.peek() { + // test_ok lazy_import_stmt_py315 + // # parse_options: {"target-version": "3.15"} + // lazy import foo + // lazy import foo as bar + // lazy from bar import baz + // lazy from sys import x as y + // lazy = 1 + // import foo as lazy + // from lazy import qux + + // test_ok lazy_import_relative_py315 + // # parse_options: {"target-version": "3.15"} + // lazy from . import basic2 + // lazy from .basic2 import x, f + // lazy from . import b, x + + // test_ok lazy_import_soft_keyword_split_py315 + // # parse_options: {"target-version": "3.15"} + // lazy + // import os + // + // lazy # comment + // from sys import path + + // test_err lazy_import_stmt_py314 + // # parse_options: {"target-version": "3.14"} + // lazy import foo + // lazy from bar import baz + TokenKind::Import => { + self.bump(TokenKind::Lazy); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::LazyImportStatement, + lazy_range, + ); + return Stmt::Import(self.parse_import_statement(start, true)); + } + TokenKind::From => { + self.bump(TokenKind::Lazy); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::LazyImportStatement, + lazy_range, + ); + return Stmt::ImportFrom(self.parse_from_import_statement(start, true)); + } + _ => {} + } + } + if token == TokenKind::Type { // Type is considered a soft keyword, so we will treat it as an identifier if // it's followed by an unexpected token. @@ -535,8 +594,7 @@ impl<'src> Parser<'src> { /// If the parser isn't positioned at an `import` token. /// /// See: - fn parse_import_statement(&mut self) -> ast::StmtImport { - let start = self.node_start(); + fn parse_import_statement(&mut self, start: TextSize, is_lazy: bool) -> ast::StmtImport { self.bump(TokenKind::Import); // test_err import_stmt_parenthesized_names @@ -563,8 +621,9 @@ impl<'src> Parser<'src> { } ast::StmtImport { - range: self.node_range(start), names, + is_lazy, + range: self.node_range(start), node_index: AtomicNodeIndex::NONE, } } @@ -576,8 +635,11 @@ impl<'src> Parser<'src> { /// If the parser isn't positioned at a `from` token. /// /// See: - fn parse_from_import_statement(&mut self) -> ast::StmtImportFrom { - let start = self.node_start(); + fn parse_from_import_statement( + &mut self, + start: TextSize, + is_lazy: bool, + ) -> ast::StmtImportFrom { self.bump(TokenKind::From); let mut leading_dots = 0; @@ -600,6 +662,7 @@ impl<'src> Parser<'src> { // from match import pattern // from type import bar // from case import pattern + // from lazy import qux // from match.type.case import foo Some(self.parse_dotted_name()) } else { @@ -676,6 +739,7 @@ impl<'src> Parser<'src> { module, names, level: leading_dots, + is_lazy, range: self.node_range(start), node_index: AtomicNodeIndex::NONE, } @@ -713,6 +777,7 @@ impl<'src> Parser<'src> { // import foo as match // import bar as case // import baz as type + // import qux as lazy Some(self.parse_identifier()) } else { // test_err import_alias_missing_asname diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap index 0e59d7d4576a9..d8a8c0c32ab81 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/debug_shadow_import.py --- ## AST @@ -26,6 +25,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Import( @@ -50,6 +50,7 @@ Module( ), }, ], + is_lazy: false, }, ), ImportFrom( @@ -76,6 +77,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -108,6 +110,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap index 06b5f87daadbb..0fe1b8a6a2fa9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/dotted_name_multiple_dots.py --- ## AST @@ -26,6 +25,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Import( @@ -44,6 +44,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Expr( diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap index a0fbe287f6118..a79c36c7777c3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/from_import_dotted_names.py --- ## AST @@ -34,6 +33,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -70,6 +70,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -156,6 +157,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap index 6534394ab29e8..4f4b86c9221ec 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/from_import_empty_names.py --- ## AST @@ -23,6 +22,7 @@ Module( ), names: [], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -38,6 +38,7 @@ Module( ), names: [], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -53,6 +54,7 @@ Module( ), names: [], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap index 7f405b3edfdbd..7eb981d4859af 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/from_import_missing_module.py --- ## AST @@ -17,6 +16,7 @@ Module( module: None, names: [], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -37,6 +37,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_rpar.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_rpar.py.snap index d1792e0e0925b..7682e69311f5b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_rpar.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_rpar.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/from_import_missing_rpar.py --- ## AST @@ -44,6 +43,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), Expr( @@ -111,6 +111,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), Expr( diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap index 616bb3d4c5f5d..56480155f1501 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/from_import_star_with_other_names.py --- ## AST @@ -44,6 +43,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -90,6 +90,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -132,6 +133,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -178,6 +180,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap index 778e7d23814f2..567b8106f2d32 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/from_import_unparenthesized_trailing_comma.py --- ## AST @@ -34,6 +33,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -66,6 +66,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -102,6 +103,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap index 33c7bf11e6c4a..7eb0677b00e49 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/import_alias_missing_asname.py --- ## AST @@ -26,6 +25,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap index 2e1c53fdbdd2f..9139ac3a86c55 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/import_from_star.py --- ## AST @@ -57,6 +56,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], @@ -99,6 +99,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], @@ -151,6 +152,7 @@ Module( }, ], level: 2, + is_lazy: false, }, ), ], @@ -213,6 +215,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap index 74adfe2d5274d..6d9db74082c85 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/import_stmt_empty.py --- ## AST @@ -15,6 +14,7 @@ Module( node_index: NodeIndex(None), range: 0..6, names: [], + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap index 2db170e781c12..eb4698a0f7bb4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/import_stmt_parenthesized_names.py --- ## AST @@ -15,6 +14,7 @@ Module( node_index: NodeIndex(None), range: 0..6, names: [], + is_lazy: false, }, ), Expr( @@ -36,6 +36,7 @@ Module( node_index: NodeIndex(None), range: 11..17, names: [], + is_lazy: false, }, ), Expr( diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap index 84de08d9600a7..f8ea1b893dcef 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/import_stmt_star_import.py --- ## AST @@ -15,6 +14,7 @@ Module( node_index: NodeIndex(None), range: 0..6, names: [], + is_lazy: false, }, ), Expr( @@ -54,6 +54,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Expr( diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap index 5179defd2f641..51a773343f44a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/import_stmt_trailing_comma.py --- ## AST @@ -15,6 +14,7 @@ Module( node_index: NodeIndex(None), range: 0..8, names: [], + is_lazy: false, }, ), Import( @@ -43,6 +43,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap index 0de649a392b81..e3731551840c1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/invalid_future_feature.py --- ## AST @@ -34,6 +33,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -70,6 +70,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -106,6 +107,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap new file mode 100644 index 0000000000000..9c85cad0b4cdd --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap @@ -0,0 +1,77 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..85, + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 44..59, + names: [ + Alias { + range: 56..59, + node_index: NodeIndex(None), + name: Identifier { + id: Name("foo"), + range: 56..59, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: true, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 60..84, + module: Some( + Identifier { + id: Name("bar"), + range: 70..73, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 81..84, + node_index: NodeIndex(None), + name: Identifier { + id: Name("baz"), + range: 81..84, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: true, + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | lazy import foo + | ^^^^ Syntax Error: Cannot use `lazy` import statement on Python 3.14 (syntax was added in Python 3.15) +3 | lazy from bar import baz + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | lazy import foo +3 | lazy from bar import baz + | ^^^^ Syntax Error: Cannot use `lazy` import statement on Python 3.14 (syntax was added in Python 3.15) + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@debug_rename_import.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@debug_rename_import.py.snap index f53b08abea87c..171741cffb699 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@debug_rename_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@debug_rename_import.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/debug_rename_import.py --- ## AST @@ -32,6 +31,7 @@ Module( ), }, ], + is_lazy: false, }, ), ImportFrom( @@ -58,6 +58,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -90,6 +91,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@dotted_name_normalized_spaces.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@dotted_name_normalized_spaces.py.snap index 68d5a3ac0e5b9..bbd24550ba998 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@dotted_name_normalized_spaces.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@dotted_name_normalized_spaces.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/dotted_name_normalized_spaces.py --- ## AST @@ -26,6 +25,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Import( @@ -44,6 +44,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_no_space.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_no_space.py.snap index dc39444395cc0..4fb7be805ef45 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_no_space.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_no_space.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/from_import_no_space.py --- ## AST @@ -28,6 +27,7 @@ Module( }, ], level: 1, + is_lazy: false, }, ), ImportFrom( @@ -48,6 +48,7 @@ Module( }, ], level: 3, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_soft_keyword_module_name.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_soft_keyword_module_name.py.snap index 6afb4d35ee291..03a302381d82a 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_soft_keyword_module_name.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_soft_keyword_module_name.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/from_import_soft_keyword_module_name.py --- ## AST @@ -8,7 +7,7 @@ input_file: crates/ruff_python_parser/resources/inline/ok/from_import_soft_keywo Module( ModModule { node_index: NodeIndex(None), - range: 0..104, + range: 0..125, body: [ ImportFrom( StmtImportFrom { @@ -34,6 +33,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -60,6 +60,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -86,32 +87,61 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( StmtImportFrom { node_index: NodeIndex(None), - range: 72..103, + range: 72..92, + module: Some( + Identifier { + id: Name("lazy"), + range: 77..81, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 89..92, + node_index: NodeIndex(None), + name: Identifier { + id: Name("qux"), + range: 89..92, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: false, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 93..124, module: Some( Identifier { id: Name("match.type.case"), - range: 77..92, + range: 98..113, node_index: NodeIndex(None), }, ), names: [ Alias { - range: 100..103, + range: 121..124, node_index: NodeIndex(None), name: Identifier { id: Name("foo"), - range: 100..103, + range: 121..124, node_index: NodeIndex(None), }, asname: None, }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_stmt_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_stmt_terminator.py.snap index 5d66543330428..28f8ab3c47b5b 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_stmt_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_stmt_terminator.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/from_import_stmt_terminator.py --- ## AST @@ -44,6 +43,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -80,6 +80,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), Expr( @@ -148,6 +149,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), Expr( @@ -216,6 +218,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), Expr( diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_as_name_soft_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_as_name_soft_keyword.py.snap index 12b0c4ea590fa..5d36cca2d300c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_as_name_soft_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_as_name_soft_keyword.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/import_as_name_soft_keyword.py --- ## AST @@ -8,7 +7,7 @@ input_file: crates/ruff_python_parser/resources/inline/ok/import_as_name_soft_ke Module( ModModule { node_index: NodeIndex(None), - range: 0..58, + range: 0..77, body: [ Import( StmtImport { @@ -32,6 +31,7 @@ Module( ), }, ], + is_lazy: false, }, ), Import( @@ -56,6 +56,7 @@ Module( ), }, ], + is_lazy: false, }, ), Import( @@ -80,6 +81,32 @@ Module( ), }, ], + is_lazy: false, + }, + ), + Import( + StmtImport { + node_index: NodeIndex(None), + range: 58..76, + names: [ + Alias { + range: 65..76, + node_index: NodeIndex(None), + name: Identifier { + id: Name("qux"), + range: 65..68, + node_index: NodeIndex(None), + }, + asname: Some( + Identifier { + id: Name("lazy"), + range: 72..76, + node_index: NodeIndex(None), + }, + ), + }, + ], + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_from_star.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_from_star.py.snap index 05896bd1e3447..1afad59a2f6a5 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_from_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_from_star.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/import_from_star.py --- ## AST @@ -34,6 +33,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_stmt_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_stmt_terminator.py.snap index 0412ecc38f7cf..dd7778aa7b8ae 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_stmt_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_stmt_terminator.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/import_stmt_terminator.py --- ## AST @@ -36,6 +35,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Import( @@ -64,6 +64,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Import( @@ -92,6 +93,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Expr( diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_relative_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_relative_py315.py.snap new file mode 100644 index 0000000000000..6ac2f0b5c7f35 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_relative_py315.py.snap @@ -0,0 +1,104 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..124, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 44..69, + module: None, + names: [ + Alias { + range: 63..69, + node_index: NodeIndex(None), + name: Identifier { + id: Name("basic2"), + range: 63..69, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 1, + is_lazy: true, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 70..99, + module: Some( + Identifier { + id: Name("basic2"), + range: 81..87, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 95..96, + node_index: NodeIndex(None), + name: Identifier { + id: Name("x"), + range: 95..96, + node_index: NodeIndex(None), + }, + asname: None, + }, + Alias { + range: 98..99, + node_index: NodeIndex(None), + name: Identifier { + id: Name("f"), + range: 98..99, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 1, + is_lazy: true, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 100..123, + module: None, + names: [ + Alias { + range: 119..120, + node_index: NodeIndex(None), + name: Identifier { + id: Name("b"), + range: 119..120, + node_index: NodeIndex(None), + }, + asname: None, + }, + Alias { + range: 122..123, + node_index: NodeIndex(None), + name: Identifier { + id: Name("x"), + range: 122..123, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 1, + is_lazy: true, + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_soft_keyword_split_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_soft_keyword_split_py315.py.snap new file mode 100644 index 0000000000000..3690cededeeb7 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_soft_keyword_split_py315.py.snap @@ -0,0 +1,89 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..97, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 44..48, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 44..48, + id: Name("lazy"), + ctx: Load, + }, + ), + }, + ), + Import( + StmtImport { + node_index: NodeIndex(None), + range: 49..58, + names: [ + Alias { + range: 56..58, + node_index: NodeIndex(None), + name: Identifier { + id: Name("os"), + range: 56..58, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: false, + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 60..64, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 60..64, + id: Name("lazy"), + ctx: Load, + }, + ), + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 76..96, + module: Some( + Identifier { + id: Name("sys"), + range: 81..84, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 92..96, + node_index: NodeIndex(None), + name: Identifier { + id: Name("path"), + range: 92..96, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: false, + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_stmt_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_stmt_py315.py.snap new file mode 100644 index 0000000000000..3c7ed37a301b0 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_stmt_py315.py.snap @@ -0,0 +1,196 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..185, + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 44..59, + names: [ + Alias { + range: 56..59, + node_index: NodeIndex(None), + name: Identifier { + id: Name("foo"), + range: 56..59, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: true, + }, + ), + Import( + StmtImport { + node_index: NodeIndex(None), + range: 60..82, + names: [ + Alias { + range: 72..82, + node_index: NodeIndex(None), + name: Identifier { + id: Name("foo"), + range: 72..75, + node_index: NodeIndex(None), + }, + asname: Some( + Identifier { + id: Name("bar"), + range: 79..82, + node_index: NodeIndex(None), + }, + ), + }, + ], + is_lazy: true, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 83..107, + module: Some( + Identifier { + id: Name("bar"), + range: 93..96, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 104..107, + node_index: NodeIndex(None), + name: Identifier { + id: Name("baz"), + range: 104..107, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: true, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 108..135, + module: Some( + Identifier { + id: Name("sys"), + range: 118..121, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 129..135, + node_index: NodeIndex(None), + name: Identifier { + id: Name("x"), + range: 129..130, + node_index: NodeIndex(None), + }, + asname: Some( + Identifier { + id: Name("y"), + range: 134..135, + node_index: NodeIndex(None), + }, + ), + }, + ], + level: 0, + is_lazy: true, + }, + ), + Assign( + StmtAssign { + node_index: NodeIndex(None), + range: 136..144, + targets: [ + Name( + ExprName { + node_index: NodeIndex(None), + range: 136..140, + id: Name("lazy"), + ctx: Store, + }, + ), + ], + value: NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 143..144, + value: Int( + 1, + ), + }, + ), + }, + ), + Import( + StmtImport { + node_index: NodeIndex(None), + range: 145..163, + names: [ + Alias { + range: 152..163, + node_index: NodeIndex(None), + name: Identifier { + id: Name("foo"), + range: 152..155, + node_index: NodeIndex(None), + }, + asname: Some( + Identifier { + id: Name("lazy"), + range: 159..163, + node_index: NodeIndex(None), + }, + ), + }, + ], + is_lazy: false, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 164..184, + module: Some( + Identifier { + id: Name("lazy"), + range: 169..173, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 181..184, + node_index: NodeIndex(None), + name: Identifier { + id: Name("qux"), + range: 181..184, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: false, + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py310.py.snap index 3213ff77434e4..1000ed1ebff8f 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py310.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/param_with_star_annotation_py310.py --- ## AST @@ -44,6 +43,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), FunctionDef( diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_with_semicolons.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_with_semicolons.py.snap index 0f302ff4d14d7..342f6cdfb2394 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_with_semicolons.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_with_semicolons.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/simple_stmts_with_semicolons.py --- ## AST @@ -33,6 +32,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), ImportFrom( @@ -59,6 +59,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), Expr( diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__from_import.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__from_import.py.snap index c962949c16809..260e2c2a09ff7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__from_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__from_import.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/valid/statement/from_import.py --- ## AST @@ -34,6 +33,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -54,6 +54,7 @@ Module( }, ], level: 1, + is_lazy: false, }, ), ImportFrom( @@ -102,6 +103,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -128,6 +130,7 @@ Module( }, ], level: 1, + is_lazy: false, }, ), ImportFrom( @@ -148,6 +151,7 @@ Module( }, ], level: 3, + is_lazy: false, }, ), ImportFrom( @@ -168,6 +172,7 @@ Module( }, ], level: 26, + is_lazy: false, }, ), ImportFrom( @@ -194,6 +199,7 @@ Module( }, ], level: 26, + is_lazy: false, }, ), ImportFrom( @@ -246,6 +252,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ImportFrom( @@ -272,6 +279,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__import.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__import.py.snap index 56388f8862860..4fa408a2f32cd 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__import.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/valid/statement/import.py --- ## AST @@ -26,6 +25,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Import( @@ -44,6 +44,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Import( @@ -68,6 +69,7 @@ Module( ), }, ], + is_lazy: false, }, ), Import( @@ -106,6 +108,7 @@ Module( asname: None, }, ], + is_lazy: false, }, ), Import( @@ -146,6 +149,7 @@ Module( ), }, ], + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_future_feature.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_future_feature.py.snap index ea367acd4a412..f64cf124e3460 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_future_feature.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_future_feature.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/ok/valid_future_feature.py --- ## AST @@ -34,6 +33,7 @@ Module( }, ], level: 0, + is_lazy: false, }, ), ], diff --git a/crates/ruff_python_semantic/src/imports.rs b/crates/ruff_python_semantic/src/imports.rs index 95c54ced42bdb..5fe5382929619 100644 --- a/crates/ruff_python_semantic/src/imports.rs +++ b/crates/ruff_python_semantic/src/imports.rs @@ -229,6 +229,7 @@ impl<'de> serde::de::Deserialize<'de> for NameImports { module, names, level, + is_lazy: _, range: _, node_index: _, }) => names @@ -246,6 +247,7 @@ impl<'de> serde::de::Deserialize<'de> for NameImports { .collect(), Stmt::Import(ast::StmtImport { names, + is_lazy: _, range: _, node_index: _, }) => names diff --git a/crates/ruff_python_trivia/src/tokenizer.rs b/crates/ruff_python_trivia/src/tokenizer.rs index 8e508d1049e24..3398064a36f2a 100644 --- a/crates/ruff_python_trivia/src/tokenizer.rs +++ b/crates/ruff_python_trivia/src/tokenizer.rs @@ -169,6 +169,7 @@ fn to_keyword_or_other(source: &str) -> SimpleTokenKind { "import" => SimpleTokenKind::Import, "in" => SimpleTokenKind::In, "is" => SimpleTokenKind::Is, + "lazy" => SimpleTokenKind::Lazy, // Lazy is a soft keyword that depends on the context but we can always lex it as a keyword and leave it to the caller (parser) to decide if it should be handled as an identifier or keyword. "lambda" => SimpleTokenKind::Lambda, "nonlocal" => SimpleTokenKind::Nonlocal, "not" => SimpleTokenKind::Not, @@ -453,6 +454,9 @@ pub enum SimpleTokenKind { /// `while` While, + /// `lazy` + Lazy, + /// `match` Match, diff --git a/crates/ty_module_resolver/src/module_name.rs b/crates/ty_module_resolver/src/module_name.rs index 49f2a71784b33..0faecf0c8b70b 100644 --- a/crates/ty_module_resolver/src/module_name.rs +++ b/crates/ty_module_resolver/src/module_name.rs @@ -308,6 +308,7 @@ impl ModuleName { module, level, names: _, + is_lazy: _, range: _, node_index: _, } = node; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7ff5c6cb6d6d7..567cdef0eb47b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8157,9 +8157,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_import_statement(&mut self, import: &ast::StmtImport) { let ast::StmtImport { + names, + is_lazy: _, range: _, node_index: _, - names, } = import; for alias in names { @@ -8369,11 +8370,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) { let ast::StmtImportFrom { - range: _, - node_index: _, module: _, names, level: _, + is_lazy: _, + range: _, + node_index: _, } = import; self.check_import_from_module_is_resolvable(import); From 735da8b415fd25438ab9650c7229a0568b170b7b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 6 Mar 2026 09:25:13 -0500 Subject: [PATCH 230/261] Reject semantic syntax errors for lazy imports (#23757) ## Summary We now reject cases like: - `lazy import ...` inside functions, including `async def` - `lazy from ... import ...` inside functions, including `async def` - `lazy import ...` and `lazy from ... import ...` inside class bodies - `lazy import ...` and `lazy from ... import ...` anywhere inside a `try` statement, including `except` / `except*` - `lazy from ... import *` anywhere - `lazy from __future__ import ...` anywhere A follow-up to #23755. See: https://github.com/astral-sh/ruff/issues/21305. --- .../semantic_errors/lazy_future_import.py | 2 + crates/ruff_linter/src/checkers/ast/mod.rs | 46 ++- crates/ruff_linter/src/importer/mod.rs | 5 +- crates/ruff_linter/src/linter.rs | 1 + crates/ruff_linter/src/rules/pyflakes/mod.rs | 7 + ...ntax_error_lazy_future_import.py_3.15.snap | 10 + .../err/lazy_import_invalid_context_py315.py | 23 ++ .../err/lazy_import_invalid_from_py315.py | 6 + .../ok/lazy_import_semantic_ok_py315.py | 6 + .../ruff_python_parser/src/semantic_errors.rs | 140 ++++++- crates/ruff_python_parser/tests/fixtures.rs | 23 +- ...@lazy_import_invalid_context_py315.py.snap | 377 ++++++++++++++++++ ...tax@lazy_import_invalid_from_py315.py.snap | 154 +++++++ ...ntax@lazy_import_semantic_ok_py315.py.snap | 176 ++++++++ crates/ty_ide/src/importer.rs | 23 +- .../diagnostics/semantic_syntax_errors.md | 15 + .../src/semantic_index/builder.rs | 34 +- 17 files changed, 1030 insertions(+), 18 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/lazy_future_import.py create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_lazy_future_import.py_3.15.snap create mode 100644 crates/ruff_python_parser/resources/inline/err/lazy_import_invalid_context_py315.py create mode 100644 crates/ruff_python_parser/resources/inline/err/lazy_import_invalid_from_py315.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/lazy_import_semantic_ok_py315.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_semantic_ok_py315.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/lazy_future_import.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/lazy_future_import.py new file mode 100644 index 0000000000000..af1d0eee5b56e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/lazy_future_import.py @@ -0,0 +1,2 @@ +lazy from __future__ import annotations +from __future__ import generator_stop diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 93b66944e3d20..3d0cdfa51101c 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -47,7 +47,8 @@ use ruff_python_ast::{PySourceType, helpers, str, visitor}; use ruff_python_codegen::{Generator, Stylist}; use ruff_python_index::Indexer; use ruff_python_parser::semantic_errors::{ - SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind, + LazyImportContext, SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, + SemanticSyntaxErrorKind, }; use ruff_python_parser::typing::{AnnotationKind, ParsedAnnotation, parse_type_annotation}; use ruff_python_parser::{ParseError, Parsed}; @@ -766,6 +767,9 @@ impl SemanticSyntaxContext for Checker<'_> { } } SemanticSyntaxErrorKind::ReboundComprehensionVariable + | SemanticSyntaxErrorKind::LazyImportNotAllowed { .. } + | SemanticSyntaxErrorKind::LazyImportStar + | SemanticSyntaxErrorKind::LazyFutureImport | SemanticSyntaxErrorKind::DuplicateTypeParameter | SemanticSyntaxErrorKind::MultipleCaseAssignment(_) | SemanticSyntaxErrorKind::IrrefutableCasePattern(_) @@ -798,6 +802,29 @@ impl SemanticSyntaxContext for Checker<'_> { self.semantic.future_annotations_or_stub() } + fn lazy_import_context(&self) -> Option { + match self.semantic.current_scope().kind { + // Possible, but invalid positions. + ScopeKind::Function(_) => return Some(LazyImportContext::Function), + ScopeKind::Class(_) => return Some(LazyImportContext::Class), + // Valid position. + ScopeKind::Module => {} + // Impossible positions because lambdas and comprehensions can't contain statements. + ScopeKind::Lambda(_) + | ScopeKind::Generator { .. } + | ScopeKind::Type + | ScopeKind::DunderClassCell => {} + } + + for statement in self.semantic.current_statements().skip(1) { + if matches!(statement, Stmt::Try(_)) { + return Some(LazyImportContext::TryExceptBlocks); + } + } + + None + } + fn in_async_context(&self) -> bool { self.semantic.in_async_context() } @@ -942,11 +969,17 @@ impl<'a> Visitor<'a> for Checker<'a> { { self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY; } - Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => { + Stmt::ImportFrom(ast::StmtImportFrom { + module, + names, + is_lazy, + .. + }) => { self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING_BOUNDARY; - // Allow __future__ imports until we see a non-__future__ import. - if let Some("__future__") = module.as_deref() { + // Allow eager `__future__` imports until we see any other import. Lazy imports, + // including `lazy from __future__ import ...`, don't enable future annotations. + if !*is_lazy && matches!(module.as_deref(), Some("__future__")) { if names .iter() .any(|alias| alias.name.as_str() == "annotations") @@ -1058,7 +1091,7 @@ impl<'a> Visitor<'a> for Checker<'a> { names, module, level, - is_lazy: _, + is_lazy, range: _, node_index: _, }) => { @@ -1068,6 +1101,7 @@ impl<'a> Visitor<'a> for Checker<'a> { let module = module.as_deref(); let level = *level; + let is_lazy = *is_lazy; // Mark the top-level module as "seen" by the semantic model. if level == 0 { @@ -1077,7 +1111,7 @@ impl<'a> Visitor<'a> for Checker<'a> { } for alias in names { - if let Some("__future__") = module { + if !is_lazy && matches!(module, Some("__future__")) { let name = alias.asname.as_ref().unwrap_or(&alias.name); self.add_binding( name, diff --git a/crates/ruff_linter/src/importer/mod.rs b/crates/ruff_linter/src/importer/mod.rs index 4bd511844750c..87c064f8ab877 100644 --- a/crates/ruff_linter/src/importer/mod.rs +++ b/crates/ruff_linter/src/importer/mod.rs @@ -540,8 +540,9 @@ impl<'a> Importer<'a> { let _docstring = body.next_if(|stmt| ast::helpers::is_docstring_stmt(stmt)); body.take_while(|stmt| { - stmt.as_import_from_stmt() - .is_some_and(|import_from| import_from.module.as_deref() == Some("__future__")) + stmt.as_import_from_stmt().is_some_and(|import_from| { + !import_from.is_lazy && import_from.module.as_deref() == Some("__future__") + }) }) .last() } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index a25dd2ad42e5a..42b7cd25f0bde 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1019,6 +1019,7 @@ mod tests { #[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)] #[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)] #[test_case(Path::new("annotated_global.py"), PythonVersion::PY314)] + #[test_case(Path::new("lazy_future_import.py"), PythonVersion::PY315)] fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> { let snapshot = format!( "semantic_syntax_error_{}_{}", diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index fb2a051ef3842..6ce7faa99e756 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -4060,6 +4060,13 @@ lambda: fu &[], ); + flakes( + r" + lazy from __future__ import annotations + ", + &[Rule::UnusedImport], + ); + flakes( r" from __future__ import annotations diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_lazy_future_import.py_3.15.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_lazy_future_import.py_3.15.snap new file mode 100644 index 0000000000000..e3d140d0d572f --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_lazy_future_import.py_3.15.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: lazy from __future__ import is not allowed + --> resources/test/fixtures/semantic_errors/lazy_future_import.py:1:1 + | +1 | lazy from __future__ import annotations + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +2 | from __future__ import generator_stop + | diff --git a/crates/ruff_python_parser/resources/inline/err/lazy_import_invalid_context_py315.py b/crates/ruff_python_parser/resources/inline/err/lazy_import_invalid_context_py315.py new file mode 100644 index 0000000000000..da5a4f53908d1 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/lazy_import_invalid_context_py315.py @@ -0,0 +1,23 @@ +# parse_options: {"target-version": "3.15"} +try: + lazy import os +except: + pass + +try: + x +except* Exception: + lazy import sys + +def func(): + lazy import math + +async def async_func(): + lazy from json import loads + +class MyClass: + lazy import typing + +def outer(): + class Inner: + lazy import json diff --git a/crates/ruff_python_parser/resources/inline/err/lazy_import_invalid_from_py315.py b/crates/ruff_python_parser/resources/inline/err/lazy_import_invalid_from_py315.py new file mode 100644 index 0000000000000..184f571e21b64 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/lazy_import_invalid_from_py315.py @@ -0,0 +1,6 @@ +# parse_options: {"target-version": "3.15"} +lazy from os import * +lazy from __future__ import annotations + +def func(): + lazy from sys import * diff --git a/crates/ruff_python_parser/resources/inline/ok/lazy_import_semantic_ok_py315.py b/crates/ruff_python_parser/resources/inline/ok/lazy_import_semantic_ok_py315.py new file mode 100644 index 0000000000000..e14aacbf156d6 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/lazy_import_semantic_ok_py315.py @@ -0,0 +1,6 @@ +# parse_options: {"target-version": "3.15"} +import contextlib +with contextlib.nullcontext(): + lazy import os +with contextlib.nullcontext(): + lazy from sys import path diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index c58ac07a3212b..d6b1b2738af00 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -57,6 +57,22 @@ impl SemanticSyntaxChecker { }); } + fn check_lazy_import_context( + ctx: &Ctx, + range: TextRange, + kind: LazyImportKind, + ) -> bool { + if let Some(context) = ctx.lazy_import_context() { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::LazyImportNotAllowed { context, kind }, + range, + ); + return true; + } + false + } + fn check_stmt(&mut self, stmt: &ast::Stmt, ctx: &Ctx) { match stmt { Stmt::ImportFrom(StmtImportFrom { @@ -64,9 +80,66 @@ impl SemanticSyntaxChecker { module, level, names, + is_lazy, .. }) => { - if matches!(module.as_deref(), Some("__future__")) { + let mut handled_lazy_error = false; + + if *is_lazy { + // test_ok lazy_import_semantic_ok_py315 + // # parse_options: {"target-version": "3.15"} + // import contextlib + // with contextlib.nullcontext(): + // lazy import os + // with contextlib.nullcontext(): + // lazy from sys import path + + // test_err lazy_import_invalid_context_py315 + // # parse_options: {"target-version": "3.15"} + // try: + // lazy import os + // except: + // pass + // + // try: + // x + // except* Exception: + // lazy import sys + // + // def func(): + // lazy import math + // + // async def async_func(): + // lazy from json import loads + // + // class MyClass: + // lazy import typing + // + // def outer(): + // class Inner: + // lazy import json + if Self::check_lazy_import_context(ctx, *range, LazyImportKind::ImportFrom) { + handled_lazy_error = true; + } else if names.iter().any(|alias| alias.name.as_str() == "*") { + // test_err lazy_import_invalid_from_py315 + // # parse_options: {"target-version": "3.15"} + // lazy from os import * + // lazy from __future__ import annotations + // + // def func(): + // lazy from sys import * + Self::add_error(ctx, SemanticSyntaxErrorKind::LazyImportStar, *range); + handled_lazy_error = true; + } else if matches!(module.as_deref(), Some("__future__")) { + Self::add_error(ctx, SemanticSyntaxErrorKind::LazyFutureImport, *range); + handled_lazy_error = true; + } + } + + if handled_lazy_error { + // Skip the regular `from`-import validations after reporting the lazy-specific + // syntax error with the highest precedence. + } else if matches!(module.as_deref(), Some("__future__")) { for name in names { if !is_known_future_feature(&name.name) { // test_ok valid_future_feature @@ -114,6 +187,13 @@ impl SemanticSyntaxChecker { } } } + Stmt::Import(ast::StmtImport { + range, + is_lazy: true, + .. + }) => { + Self::check_lazy_import_context(ctx, *range, LazyImportKind::Import); + } Stmt::Match(match_stmt) => { Self::irrefutable_match_case(match_stmt, ctx); for case in &match_stmt.cases { @@ -748,9 +828,12 @@ impl SemanticSyntaxChecker { match stmt { Stmt::Expr(StmtExpr { value, .. }) if !self.seen_module_docstring_boundary && value.is_string_literal_expr() => {} - Stmt::ImportFrom(StmtImportFrom { module, .. }) => { - // Allow __future__ imports until we see a non-__future__ import. - if !matches!(module.as_deref(), Some("__future__")) { + Stmt::ImportFrom(StmtImportFrom { + module, is_lazy, .. + }) => { + // Allow eager `__future__` imports until we see any other import. Lazy imports, + // including `lazy from __future__ import ...`, always close the boundary. + if *is_lazy || !matches!(module.as_deref(), Some("__future__")) { self.seen_futures_boundary = true; } } @@ -1114,6 +1197,19 @@ fn is_known_future_feature(name: &str) -> bool { ) } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum LazyImportKind { + Import, + ImportFrom, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum LazyImportContext { + Function, + Class, + TryExceptBlocks, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub struct SemanticSyntaxError { pub kind: SemanticSyntaxErrorKind, @@ -1231,6 +1327,24 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => { write!(f, "Future feature `{name}` is not defined") } + SemanticSyntaxErrorKind::LazyImportNotAllowed { context, kind } => { + let statement = match kind { + LazyImportKind::Import => "lazy import", + LazyImportKind::ImportFrom => "lazy from ... import", + }; + let location = match context { + LazyImportContext::Function => "functions", + LazyImportContext::Class => "classes", + LazyImportContext::TryExceptBlocks => "try/except blocks", + }; + write!(f, "{statement} not allowed inside {location}") + } + SemanticSyntaxErrorKind::LazyImportStar => { + f.write_str("lazy from ... import * is not allowed") + } + SemanticSyntaxErrorKind::LazyFutureImport => { + f.write_str("lazy from __future__ import is not allowed") + } SemanticSyntaxErrorKind::BreakOutsideLoop => f.write_str("`break` outside loop"), SemanticSyntaxErrorKind::ContinueOutsideLoop => f.write_str("`continue` outside loop"), SemanticSyntaxErrorKind::GlobalParameter(name) => { @@ -1257,6 +1371,18 @@ impl Ranged for SemanticSyntaxError { #[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum SemanticSyntaxErrorKind { + /// Represents a `lazy` import statement in an invalid context. + LazyImportNotAllowed { + context: LazyImportContext, + kind: LazyImportKind, + }, + + /// Represents the use of `lazy from ... import *`. + LazyImportStar, + + /// Represents the use of `lazy from __future__ import ...`. + LazyFutureImport, + /// Represents the use of a `__future__` import after the beginning of a file. /// /// ## Examples @@ -2131,6 +2257,12 @@ pub trait SemanticSyntaxContext { /// Returns `true` if `__future__`-style type annotations are enabled. fn future_annotations_or_stub(&self) -> bool; + /// Returns the nearest invalid context for a `lazy` import statement, if any. + /// + /// This should return the innermost relevant restriction in order of precedence: + /// function, class, then `try`/`except`. + fn lazy_import_context(&self) -> Option; + /// The target Python version for detecting backwards-incompatible syntax changes. fn python_version(&self) -> PythonVersion; diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 0f29c46970f36..0655859838a6c 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -10,7 +10,7 @@ use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_module}; use ruff_python_ast::{self as ast, AnyNodeRef, Mod, PythonVersion}; use ruff_python_parser::semantic_errors::{ - SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, + LazyImportContext, SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, }; use ruff_python_parser::{Mode, ParseErrorType, ParseOptions, Parsed, parse_unchecked}; use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; @@ -532,6 +532,7 @@ struct SemanticSyntaxCheckerVisitor<'a> { python_version: PythonVersion, source: &'a str, scopes: Vec, + in_try: bool, } impl<'a> SemanticSyntaxCheckerVisitor<'a> { @@ -542,6 +543,7 @@ impl<'a> SemanticSyntaxCheckerVisitor<'a> { python_version: PythonVersion::default(), source, scopes: vec![Scope::Module], + in_try: false, } } @@ -567,6 +569,20 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { false } + fn lazy_import_context(&self) -> Option { + match self.scopes.last() { + Some(Scope::Function { .. }) => return Some(LazyImportContext::Function), + Some(Scope::Class) => return Some(LazyImportContext::Class), + Some(Scope::Module | Scope::Comprehension { .. }) | None => {} + } + + if self.in_try { + return Some(LazyImportContext::TryExceptBlocks); + } + + None + } + fn python_version(&self) -> PythonVersion { self.python_version } @@ -672,6 +688,11 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { ast::visitor::walk_stmt(self, stmt); self.scopes.pop().unwrap(); } + ast::Stmt::Try(_) => { + let was_in_try = std::mem::replace(&mut self.in_try, true); + ast::visitor::walk_stmt(self, stmt); + self.in_try = was_in_try; + } _ => { ast::visitor::walk_stmt(self, stmt); } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap new file mode 100644 index 0000000000000..3346083db6d13 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap @@ -0,0 +1,377 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..322, + body: [ + Try( + StmtTry { + node_index: NodeIndex(None), + range: 44..84, + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 53..67, + names: [ + Alias { + range: 65..67, + node_index: NodeIndex(None), + name: Identifier { + id: Name("os"), + range: 65..67, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: true, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 68..84, + node_index: NodeIndex(None), + type_: None, + name: None, + body: [ + Pass( + StmtPass { + node_index: NodeIndex(None), + range: 80..84, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + Try( + StmtTry { + node_index: NodeIndex(None), + range: 86..135, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 95..96, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 95..96, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 97..135, + node_index: NodeIndex(None), + type_: Some( + Name( + ExprName { + node_index: NodeIndex(None), + range: 105..114, + id: Name("Exception"), + ctx: Load, + }, + ), + ), + name: None, + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 120..135, + names: [ + Alias { + range: 132..135, + node_index: NodeIndex(None), + name: Identifier { + id: Name("sys"), + range: 132..135, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: true, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: true, + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 137..169, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("func"), + range: 141..145, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 145..147, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 153..169, + names: [ + Alias { + range: 165..169, + node_index: NodeIndex(None), + name: Identifier { + id: Name("math"), + range: 165..169, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: true, + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 171..226, + is_async: true, + decorator_list: [], + name: Identifier { + id: Name("async_func"), + range: 181..191, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 191..193, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 199..226, + module: Some( + Identifier { + id: Name("json"), + range: 209..213, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 221..226, + node_index: NodeIndex(None), + name: Identifier { + id: Name("loads"), + range: 221..226, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: true, + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + node_index: NodeIndex(None), + range: 228..265, + decorator_list: [], + name: Identifier { + id: Name("MyClass"), + range: 234..241, + node_index: NodeIndex(None), + }, + type_params: None, + arguments: None, + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 247..265, + names: [ + Alias { + range: 259..265, + node_index: NodeIndex(None), + name: Identifier { + id: Name("typing"), + range: 259..265, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: true, + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 267..321, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("outer"), + range: 271..276, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 276..278, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + ClassDef( + StmtClassDef { + node_index: NodeIndex(None), + range: 284..321, + decorator_list: [], + name: Identifier { + id: Name("Inner"), + range: 290..295, + node_index: NodeIndex(None), + }, + type_params: None, + arguments: None, + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 305..321, + names: [ + Alias { + range: 317..321, + node_index: NodeIndex(None), + name: Identifier { + id: Name("json"), + range: 317..321, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: true, + }, + ), + ], + }, + ), + ], + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | try: +3 | lazy import os + | ^^^^^^^^^^^^^^ Syntax Error: lazy import not allowed inside try/except blocks +4 | except: +5 | pass + | + + + | + 8 | x + 9 | except* Exception: +10 | lazy import sys + | ^^^^^^^^^^^^^^^ Syntax Error: lazy import not allowed inside try/except blocks +11 | +12 | def func(): + | + + + | +12 | def func(): +13 | lazy import math + | ^^^^^^^^^^^^^^^^ Syntax Error: lazy import not allowed inside functions +14 | +15 | async def async_func(): + | + + + | +15 | async def async_func(): +16 | lazy from json import loads + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: lazy from ... import not allowed inside functions +17 | +18 | class MyClass: + | + + + | +18 | class MyClass: +19 | lazy import typing + | ^^^^^^^^^^^^^^^^^^ Syntax Error: lazy import not allowed inside classes +20 | +21 | def outer(): + | + + + | +21 | def outer(): +22 | class Inner: +23 | lazy import json + | ^^^^^^^^^^^^^^^^ Syntax Error: lazy import not allowed inside classes + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap new file mode 100644 index 0000000000000..57b7e5b6f0e93 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap @@ -0,0 +1,154 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..146, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 44..65, + module: Some( + Identifier { + id: Name("os"), + range: 54..56, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 64..65, + node_index: NodeIndex(None), + name: Identifier { + id: Name("*"), + range: 64..65, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: true, + }, + ), + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 66..105, + module: Some( + Identifier { + id: Name("__future__"), + range: 76..86, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 94..105, + node_index: NodeIndex(None), + name: Identifier { + id: Name("annotations"), + range: 94..105, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: true, + }, + ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 107..145, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("func"), + range: 111..115, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 115..117, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 123..145, + module: Some( + Identifier { + id: Name("sys"), + range: 133..136, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 144..145, + node_index: NodeIndex(None), + name: Identifier { + id: Name("*"), + range: 144..145, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: true, + }, + ), + ], + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | # parse_options: {"target-version": "3.15"} +2 | lazy from os import * + | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: lazy from ... import * is not allowed +3 | lazy from __future__ import annotations + | + + + | +1 | # parse_options: {"target-version": "3.15"} +2 | lazy from os import * +3 | lazy from __future__ import annotations + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: lazy from __future__ import is not allowed +4 | +5 | def func(): + | + + + | +5 | def func(): +6 | lazy from sys import * + | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: lazy from ... import not allowed inside functions + | + + + | +5 | def func(): +6 | lazy from sys import * + | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from sys import *` only allowed at module level + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_semantic_ok_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_semantic_ok_py315.py.snap new file mode 100644 index 0000000000000..646867677b32a --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lazy_import_semantic_ok_py315.py.snap @@ -0,0 +1,176 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..173, + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 44..61, + names: [ + Alias { + range: 51..61, + node_index: NodeIndex(None), + name: Identifier { + id: Name("contextlib"), + range: 51..61, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: false, + }, + ), + With( + StmtWith { + node_index: NodeIndex(None), + range: 62..111, + is_async: false, + items: [ + WithItem { + range: 67..91, + node_index: NodeIndex(None), + context_expr: Call( + ExprCall { + node_index: NodeIndex(None), + range: 67..91, + func: Attribute( + ExprAttribute { + node_index: NodeIndex(None), + range: 67..89, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 67..77, + id: Name("contextlib"), + ctx: Load, + }, + ), + attr: Identifier { + id: Name("nullcontext"), + range: 78..89, + node_index: NodeIndex(None), + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 89..91, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + optional_vars: None, + }, + ], + body: [ + Import( + StmtImport { + node_index: NodeIndex(None), + range: 97..111, + names: [ + Alias { + range: 109..111, + node_index: NodeIndex(None), + name: Identifier { + id: Name("os"), + range: 109..111, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + is_lazy: true, + }, + ), + ], + }, + ), + With( + StmtWith { + node_index: NodeIndex(None), + range: 112..172, + is_async: false, + items: [ + WithItem { + range: 117..141, + node_index: NodeIndex(None), + context_expr: Call( + ExprCall { + node_index: NodeIndex(None), + range: 117..141, + func: Attribute( + ExprAttribute { + node_index: NodeIndex(None), + range: 117..139, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 117..127, + id: Name("contextlib"), + ctx: Load, + }, + ), + attr: Identifier { + id: Name("nullcontext"), + range: 128..139, + node_index: NodeIndex(None), + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 139..141, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + optional_vars: None, + }, + ], + body: [ + ImportFrom( + StmtImportFrom { + node_index: NodeIndex(None), + range: 147..172, + module: Some( + Identifier { + id: Name("sys"), + range: 157..160, + node_index: NodeIndex(None), + }, + ), + names: [ + Alias { + range: 168..172, + node_index: NodeIndex(None), + name: Identifier { + id: Name("path"), + range: 168..172, + node_index: NodeIndex(None), + }, + asname: None, + }, + ], + level: 0, + is_lazy: true, + }, + ), + ], + }, + ), + ], + }, +) +``` diff --git a/crates/ty_ide/src/importer.rs b/crates/ty_ide/src/importer.rs index 565255bf24caf..480437d86ee6b 100644 --- a/crates/ty_ide/src/importer.rs +++ b/crates/ty_ide/src/importer.rs @@ -301,7 +301,9 @@ impl<'a> Importer<'a> { import .stmt .as_import_from_stmt() - .is_some_and(|import_from| import_from.module.as_deref() == Some("__future__")) + .is_some_and(|import_from| { + !import_from.is_lazy && import_from.module.as_deref() == Some("__future__") + }) }) .last() } @@ -1459,6 +1461,25 @@ from __future__ import annotations "#); } + #[test] + fn lazy_future_import_is_not_special() { + // Lazy `__future__` imports must not act like real future-import anchors for insertion. + let test = cursor_test( + "\ +lazy from __future__ import annotations + + + ", + ); + assert_snapshot!( + test.import("typing", "TypeVar"), @" + import typing + lazy from __future__ import annotations + + typing.TypeVar + "); + } + #[test] fn qualify_symbol_to_avoid_overwriting_other_symbol_in_scope() { let test = cursor_test( diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index 0b1e5b69b990e..7212217e0ad86 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -70,6 +70,21 @@ from collections import namedtuple from __future__ import print_function ``` +## Lazy `__future__` imports are not future imports + +```toml +[environment] +python-version = "3.15" +``` + +```py +# error: [invalid-syntax] "lazy from __future__ import is not allowed" +lazy from __future__ import annotations + +# error: [invalid-syntax] "__future__ imports must be at the top of the file" +from __future__ import generator_stop +``` + ## Invalid annotation This one might be a bit redundant with the `invalid-type-form` error. diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 8498c2ffe100f..ea24d8594a6f0 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -13,8 +13,8 @@ use ruff_python_ast::name::Name; use ruff_python_ast::visitor::{Visitor, walk_expr, walk_pattern, walk_stmt}; use ruff_python_ast::{self as ast, AtomicNodeIndex, NodeIndex, PySourceType, PythonVersion}; use ruff_python_parser::semantic_errors::{ - SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind, - YieldOutsideFunctionKind, + LazyImportContext, SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, + SemanticSyntaxErrorKind, YieldOutsideFunctionKind, }; use ruff_text_size::TextRange; use ty_module_resolver::{ModuleName, resolve_module}; @@ -114,6 +114,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { python_version: PythonVersion, source_text: OnceCell, semantic_checker: SemanticSyntaxChecker, + in_try: bool, // Semantic Index fields scopes: IndexVec, @@ -173,6 +174,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { python_version: Program::get(db).python_version(db), source_text: OnceCell::new(), semantic_checker: SemanticSyntaxChecker::default(), + in_try: false, semantic_syntax_errors: RefCell::default(), }; @@ -1956,12 +1958,13 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { (&alias.name.id, is_self_import) }; - // Look for imports `from __future__ import annotations`, ignore `as ...` + // Look for eager imports `from __future__ import annotations`, ignore `as ...` // We intentionally don't enforce the rules about location of `__future__` // imports here, we assume the user's intent was to apply the `__future__` // import, so we still check using it (and will also emit a diagnostic about a // miss-placed `__future__` import.) - self.has_future_annotations |= alias.name.id == "annotations" + self.has_future_annotations |= !node.is_lazy + && alias.name.id == "annotations" && node.module.as_deref() == Some("__future__"); let symbol = self.add_symbol(symbol_name.clone()); @@ -2505,6 +2508,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { range: _, node_index: _, }) => { + let was_in_try = std::mem::replace(&mut self.in_try, true); self.record_ambiguous_reachability(); // Save the state prior to visiting any of the `try` block. @@ -2614,6 +2618,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { // - https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d // - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702 self.visit_body(finalbody); + self.in_try = was_in_try; } ast::Stmt::Raise(_) | ast::Stmt::Return(_) => { @@ -3227,6 +3232,27 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { self.has_future_annotations } + fn lazy_import_context(&self) -> Option { + match self.scopes[self.current_scope()].kind() { + // Possible, but invalid positions. + ScopeKind::Function => return Some(LazyImportContext::Function), + ScopeKind::Class => return Some(LazyImportContext::Class), + // Valid position. + ScopeKind::Module => {} + // Impossible positions because lambdas and comprehensions can't contain statements. + ScopeKind::Comprehension + | ScopeKind::Lambda + | ScopeKind::TypeAlias + | ScopeKind::TypeParams => {} + } + + if self.in_try { + return Some(LazyImportContext::TryExceptBlocks); + } + + None + } + fn python_version(&self) -> PythonVersion { self.python_version } From 7365268427af342e1c88f0d9b2867b28adc74bc3 Mon Sep 17 00:00:00 2001 From: xvchris <56232580+xvchris@users.noreply.github.com> Date: Sat, 7 Mar 2026 02:49:59 +0800 Subject: [PATCH 231/261] Support `newline` parameter in FURB101 for Python 3.13+ (#23754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #23748. Python 3.13 added the `newline` parameter to [`pathlib.Path.read_text()`](https://docs.python.org/3.13/library/pathlib.html#pathlib.Path.read_text), so `open(..., newline=...) + read()` can now be replaced by `Path(...).read_text(newline=...)` when targeting Python 3.13 or later. Previously, the presence of `newline` in read mode would unconditionally suppress the FURB101 diagnostic. This PR makes the suppression conditional on the target Python version: `newline` is only rejected for `target-version < py313`. ## Changes - **`helpers.rs`**: Updated `match_open_keywords` to allow `newline` in read mode when `target_version >= PY313`, mirroring how write mode already conditionally allows `newline` for `>= PY310`. - **`FURB101_0.py`**: Updated comments for the existing newline test cases (now detected as errors since the default test uses `PythonVersion::latest()`). - **`FURB101_3.py`**: New test fixture exercising `newline` with various values (`"\r\n"`, `""`, `None`). - **`mod.rs`**: Added two new tests: - `read_whole_file_newline_python_313` — verifies FURB101 fires on Python 3.13+ - `read_whole_file_newline_python_312` — verifies FURB101 is suppressed on Python 3.12 ## Test Plan ``` cargo insta test -p ruff_linter --accept -- refurb::tests # 45 passed; 0 failed ``` --------- Co-authored-by: xvchris Co-authored-by: Brent Westbrook --- .../test/fixtures/refurb/FURB101_0.py | 4 +- .../test/fixtures/refurb/FURB101_3.py | 13 +++ .../ruff_linter/src/rules/refurb/helpers.rs | 8 +- crates/ruff_linter/src/rules/refurb/mod.rs | 14 +++- ...__refurb__tests__FURB101_FURB101_0.py.snap | 48 +++++++++++ ...hole_file_newline_python_version_diff.snap | 81 +++++++++++++++++++ 6 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/refurb/FURB101_3.py create mode 100644 crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__read_whole_file_newline_python_version_diff.snap diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_0.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_0.py index 83f864a6e8e9e..9d971d2c77a37 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_0.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_0.py @@ -81,11 +81,11 @@ def bar(x): with open("file.txt", buffering=1) as f: x = f.read() -# force CRLF, not supported in read_text() +# FURB101 (newline is supported in read_text on Python 3.13+) with open("file.txt", newline="\r\n") as f: x = f.read() -# dont mistake "newline" for "mode" +# FURB101 (dont mistake "newline" for "mode") with open("file.txt", newline="b") as f: x = f.read() diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_3.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_3.py new file mode 100644 index 0000000000000..e17b2b3c57dad --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_3.py @@ -0,0 +1,13 @@ +# Tests for Python 3.13+ where `pathlib.Path.read_text` supports `newline`. + +# FURB101 (newline is supported in read_text on Python 3.13+) +with open("file.txt", newline="\r\n") as f: + x = f.read() + +# FURB101 (newline with encoding) +with open("file.txt", encoding="utf-8", newline="") as f: + x = f.read() + +# FURB101 (newline=None is also valid) +with open("file.txt", newline=None) as f: + x = f.read() diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index 70666e43b49b0..0e1d13526700d 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -366,10 +366,12 @@ fn match_open_keywords( "encoding" | "errors" => result.push(keyword), "newline" => { if read_mode { - // newline is only valid for write_text - return None; + if target_version < PythonVersion::PY313 { + // `pathlib.Path.read_text` doesn't support `newline` until Python 3.13. + return None; + } } else if target_version < PythonVersion::PY310 { - // `pathlib` doesn't support `newline` until Python 3.10. + // `pathlib.Path.write_text` doesn't support `newline` until Python 3.10. return None; } diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 2e32970a4028b..72a30f67609f5 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -13,7 +13,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_diagnostics, settings}; + use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; #[test_case(Rule::ReadWholeFile, Path::new("FURB101_0.py"))] #[test_case(Rule::ReadWholeFile, Path::new("FURB101_1.py"))] @@ -66,6 +66,18 @@ mod tests { Ok(()) } + #[test] + fn read_whole_file_newline_python_version_diff() -> Result<()> { + assert_diagnostics_diff!( + Path::new("refurb/FURB101_3.py"), + &settings::LinterSettings::for_rule(Rule::ReadWholeFile) + .with_target_version(PythonVersion::PY313), + &settings::LinterSettings::for_rule(Rule::ReadWholeFile) + .with_target_version(PythonVersion::PY312), + ); + Ok(()) + } + #[test] fn write_whole_file_python_39() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_0.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_0.py.snap index 4d1b9b8edf9d3..6cb408b8d7781 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_0.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_0.py.snap @@ -190,6 +190,54 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()` | help: Replace with `Path("file.txt").read_text()` +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(newline="\r\n")` + --> FURB101_0.py:85:6 + | +84 | # FURB101 (newline is supported in read_text on Python 3.13+) +85 | with open("file.txt", newline="\r\n") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +86 | x = f.read() + | +help: Replace with `Path("file.txt").read_text(newline="\r\n")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +83 | x = f.read() +84 | +85 | # FURB101 (newline is supported in read_text on Python 3.13+) + - with open("file.txt", newline="\r\n") as f: + - x = f.read() +86 + x = pathlib.Path("file.txt").read_text(newline="\r\n") +87 | +88 | # FURB101 (dont mistake "newline" for "mode") +89 | with open("file.txt", newline="b") as f: + +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(newline="b")` + --> FURB101_0.py:89:6 + | +88 | # FURB101 (dont mistake "newline" for "mode") +89 | with open("file.txt", newline="b") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +90 | x = f.read() + | +help: Replace with `Path("file.txt").read_text(newline="b")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +87 | x = f.read() +88 | +89 | # FURB101 (dont mistake "newline" for "mode") + - with open("file.txt", newline="b") as f: + - x = f.read() +90 + x = pathlib.Path("file.txt").read_text(newline="b") +91 | +92 | # I guess we can possibly also report this case, but the question +93 | # is why the user would put "r+" here in the first place. + FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")` --> FURB101_0.py:130:6 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__read_whole_file_newline_python_version_diff.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__read_whole_file_newline_python_version_diff.snap new file mode 100644 index 0000000000000..182507158727a --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__read_whole_file_newline_python_version_diff.snap @@ -0,0 +1,81 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +--- Linter settings --- +-linter.unresolved_target_version = 3.13 ++linter.unresolved_target_version = 3.12 + +--- Summary --- +Removed: 3 +Added: 0 + +--- Removed --- +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(newline="\r\n")` + --> FURB101_3.py:4:6 + | +3 | # FURB101 (newline is supported in read_text on Python 3.13+) +4 | with open("file.txt", newline="\r\n") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +5 | x = f.read() + | +help: Replace with `Path("file.txt").read_text(newline="\r\n")` +1 | # Tests for Python 3.13+ where `pathlib.Path.read_text` supports `newline`. +2 | +3 | # FURB101 (newline is supported in read_text on Python 3.13+) + - with open("file.txt", newline="\r\n") as f: + - x = f.read() +4 + import pathlib +5 + x = pathlib.Path("file.txt").read_text(newline="\r\n") +6 | +7 | # FURB101 (newline with encoding) +8 | with open("file.txt", encoding="utf-8", newline="") as f: + + +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8", newline="")` + --> FURB101_3.py:8:6 + | +7 | # FURB101 (newline with encoding) +8 | with open("file.txt", encoding="utf-8", newline="") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +9 | x = f.read() + | +help: Replace with `Path("file.txt").read_text(encoding="utf-8", newline="")` +1 | # Tests for Python 3.13+ where `pathlib.Path.read_text` supports `newline`. +2 | +3 | # FURB101 (newline is supported in read_text on Python 3.13+) +4 + import pathlib +5 | with open("file.txt", newline="\r\n") as f: +6 | x = f.read() +7 | +8 | # FURB101 (newline with encoding) + - with open("file.txt", encoding="utf-8", newline="") as f: + - x = f.read() +9 + x = pathlib.Path("file.txt").read_text(encoding="utf-8", newline="") +10 | +11 | # FURB101 (newline=None is also valid) +12 | with open("file.txt", newline=None) as f: + + +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(newline=None)` + --> FURB101_3.py:12:6 + | +11 | # FURB101 (newline=None is also valid) +12 | with open("file.txt", newline=None) as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +13 | x = f.read() + | +help: Replace with `Path("file.txt").read_text(newline=None)` +1 | # Tests for Python 3.13+ where `pathlib.Path.read_text` supports `newline`. +2 | +3 | # FURB101 (newline is supported in read_text on Python 3.13+) +4 + import pathlib +5 | with open("file.txt", newline="\r\n") as f: +6 | x = f.read() +7 | +-------------------------------------------------------------------------------- +10 | x = f.read() +11 | +12 | # FURB101 (newline=None is also valid) + - with open("file.txt", newline=None) as f: + - x = f.read() +13 + x = pathlib.Path("file.txt").read_text(newline=None) From 46507b81124745cc8ca0997534907512550cb9a7 Mon Sep 17 00:00:00 2001 From: Anish Giri <161533316+anishgirianish@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:18:32 -0600 Subject: [PATCH 232/261] [`perflint`] Fix comment duplication in fixes (`PERF401`, `PERF403`) (#23729) ## Summary - Fix comments inside if test and for target being duplicated when transforming a for-loop into a comprehension ## Test Plan - Added test cases for both PERF401 and PERF403 - Verified comments appear exactly once in preview snapshot fixes ## Closes: [#18787](https://github.com/astral-sh/ruff/issues/18787) --- .../test/fixtures/perflint/PERF401.py | 23 ++++++- .../test/fixtures/perflint/PERF403.py | 24 +++++++- .../rules/manual_dict_comprehension.rs | 16 +++-- .../rules/manual_list_comprehension.rs | 15 +++-- ...__perflint__tests__PERF401_PERF401.py.snap | 22 +++++++ ...__perflint__tests__PERF403_PERF403.py.snap | 26 +++++++- ...t__tests__preview__PERF401_PERF401.py.snap | 50 +++++++++++++++ ...t__tests__preview__PERF403_PERF403.py.snap | 61 +++++++++++++++++-- 8 files changed, 216 insertions(+), 21 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py index 21f3a34873af5..b0e17135f323d 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py @@ -304,4 +304,25 @@ def x(): nonlocal NL_INDEX result = [] for NL_INDEX in range(3): - result.append(NL_INDEX) \ No newline at end of file + result.append(NL_INDEX) + +def f(): + # comment duplication in if test (https://github.com/astral-sh/ruff/issues/18787) + original = list(range(10000)) + filtered = [] + for i in original: + if ( + i + # comment + ): + filtered.append(i) + +def f(): + # comment duplication in target (https://github.com/astral-sh/ruff/issues/18787) + original = list(range(10000)) + filtered = [] + for ( + i # comment + ) in original: + if i > 0: + filtered.append(i) \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py index a393823e61081..1a39db2af94c8 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py @@ -210,6 +210,24 @@ def issue_19153_2(): def issue_19153_3(): v = {} - for o, (x,) in ["ox"]: - v[(x,)] = o - return v \ No newline at end of file + for o, (x,) in ["ox"]: + v[(x,)] = o + return v + +def f(): + # comment duplication in if test (https://github.com/astral-sh/ruff/issues/18787) + result = {} + for k in ["a", "b", "c"]: + if ( + k + # comment + ): + result[k] = k + +def f(): + # comment duplication in target (https://github.com/astral-sh/ruff/issues/18787) + result = {} + for ( + k # comment + ) in ["a", "b", "c"]: + result[k] = k \ No newline at end of file diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs index 560aac29a8d44..b041c3a4f2962 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs @@ -387,11 +387,17 @@ fn convert_to_dict_comprehension( let comprehension_str = format!("{{{key_str}: {value_str} {for_type} {target_str} in {iter_str}{if_str}}}"); - let for_loop_inline_comments = comment_strings_in_range( - checker, - for_stmt.range, - &[key.range(), value.range(), for_stmt.iter.range()], - ); + let mut ranges_to_ignore = vec![ + key.range(), + value.range(), + for_stmt.iter.range(), + for_stmt.target.range(), + ]; + if let Some(test) = if_test { + ranges_to_ignore.push(test.range()); + } + let for_loop_inline_comments = + comment_strings_in_range(checker, for_stmt.range, &ranges_to_ignore); let newline = checker.stylist().line_ending().as_str(); diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs index 84b7290e51499..1c0898a4a8a2e 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs @@ -422,11 +422,16 @@ fn convert_to_list_extend( }; let variable_name = locator.slice(binding); - let for_loop_inline_comments = comment_strings_in_range( - checker, - for_stmt.range, - &[to_append.range(), for_stmt.iter.range()], - ); + let mut ranges_to_ignore = vec![ + to_append.range(), + for_stmt.iter.range(), + for_stmt.target.range(), + ]; + if let Some(test) = if_test { + ranges_to_ignore.push(test.range()); + } + let for_loop_inline_comments = + comment_strings_in_range(checker, for_stmt.range, &ranges_to_ignore); let newline = checker.stylist().line_ending().as_str(); diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap index e884d518376ad..9b92595c24387 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap @@ -294,3 +294,25 @@ PERF401 Use a list comprehension to create a transformed list 294 | G_INDEX = None | help: Replace for loop with list comprehension + +PERF401 Use a list comprehension to create a transformed list + --> PERF401.py:318:13 + | +316 | # comment +317 | ): +318 | filtered.append(i) + | ^^^^^^^^^^^^^^^^^^ +319 | +320 | def f(): + | +help: Replace for loop with list comprehension + +PERF401 Use a list comprehension to create a transformed list + --> PERF401.py:328:13 + | +326 | ) in original: +327 | if i > 0: +328 | filtered.append(i) + | ^^^^^^^^^^^^^^^^^^ + | +help: Replace for loop with list comprehension diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap index fa06705416706..4a83c843e7e51 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap @@ -203,9 +203,31 @@ PERF403 Use a dictionary comprehension instead of a for-loop --> PERF403.py:214:9 | 212 | v = {} -213 | for o, (x,) in ["ox"]: -214 | v[(x,)] = o +213 | for o, (x,) in ["ox"]: +214 | v[(x,)] = o | ^^^^^^^^^^^ 215 | return v | help: Replace for loop with dict comprehension + +PERF403 Use a dictionary comprehension instead of a for-loop + --> PERF403.py:225:13 + | +223 | # comment +224 | ): +225 | result[k] = k + | ^^^^^^^^^^^^^ +226 | +227 | def f(): + | +help: Replace for loop with dict comprehension + +PERF403 Use a dictionary comprehension instead of a for-loop + --> PERF403.py:233:9 + | +231 | k # comment +232 | ) in ["a", "b", "c"]: +233 | result[k] = k + | ^^^^^^^^^^^^^ + | +help: Replace for loop with dict comprehension diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap index 3d114bcd3aef6..2762aed677b23 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap @@ -628,3 +628,53 @@ help: Replace for loop with list comprehension 292 | G_INDEX = None 293 | def f(): note: This is an unsafe fix and may change runtime behavior + +PERF401 [*] Use a list comprehension to create a transformed list + --> PERF401.py:318:13 + | +316 | # comment +317 | ): +318 | filtered.append(i) + | ^^^^^^^^^^^^^^^^^^ +319 | +320 | def f(): + | +help: Replace for loop with list comprehension +309 | def f(): +310 | # comment duplication in if test (https://github.com/astral-sh/ruff/issues/18787) +311 | original = list(range(10000)) + - filtered = [] + - for i in original: + - if ( + - i + - # comment + - ): + - filtered.append(i) +312 + # comment +313 + filtered = [i for i in original if i] +314 | +315 | def f(): +316 | # comment duplication in target (https://github.com/astral-sh/ruff/issues/18787) +note: This is an unsafe fix and may change runtime behavior + +PERF401 [*] Use a list comprehension to create a transformed list + --> PERF401.py:328:13 + | +326 | ) in original: +327 | if i > 0: +328 | filtered.append(i) + | ^^^^^^^^^^^^^^^^^^ + | +help: Replace for loop with list comprehension +320 | def f(): +321 | # comment duplication in target (https://github.com/astral-sh/ruff/issues/18787) +322 | original = list(range(10000)) + - filtered = [] + - for ( + - i # comment + - ) in original: + - if i > 0: + - filtered.append(i) +323 + # comment +324 + filtered = [i for i in original if i > 0] +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap index 810e014b2e858..90b790bc5531f 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap @@ -426,8 +426,8 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop --> PERF403.py:214:9 | 212 | v = {} -213 | for o, (x,) in ["ox"]: -214 | v[(x,)] = o +213 | for o, (x,) in ["ox"]: +214 | v[(x,)] = o | ^^^^^^^^^^^ 215 | return v | @@ -436,8 +436,59 @@ help: Replace for loop with dict comprehension 210 | 211 | def issue_19153_3(): - v = {} - - for o, (x,) in ["ox"]: - - v[(x,)] = o -212 + v = {(x,): o for o, (x,) in ["ox"]} + - for o, (x,) in ["ox"]: + - v[(x,)] = o +212 + v = {(x,): o for o, (x,) in ["ox"]} 213 | return v +214 | +215 | def f(): +note: This is an unsafe fix and may change runtime behavior + +PERF403 [*] Use a dictionary comprehension instead of a for-loop + --> PERF403.py:225:13 + | +223 | # comment +224 | ): +225 | result[k] = k + | ^^^^^^^^^^^^^ +226 | +227 | def f(): + | +help: Replace for loop with dict comprehension +216 | +217 | def f(): +218 | # comment duplication in if test (https://github.com/astral-sh/ruff/issues/18787) + - result = {} + - for k in ["a", "b", "c"]: + - if ( + - k + - # comment + - ): + - result[k] = k +219 + # comment +220 + result = {k: k for k in ["a", "b", "c"] if k} +221 | +222 | def f(): +223 | # comment duplication in target (https://github.com/astral-sh/ruff/issues/18787) +note: This is an unsafe fix and may change runtime behavior + +PERF403 [*] Use a dictionary comprehension instead of a for-loop + --> PERF403.py:233:9 + | +231 | k # comment +232 | ) in ["a", "b", "c"]: +233 | result[k] = k + | ^^^^^^^^^^^^^ + | +help: Replace for loop with dict comprehension +226 | +227 | def f(): +228 | # comment duplication in target (https://github.com/astral-sh/ruff/issues/18787) + - result = {} + - for ( + - k # comment + - ) in ["a", "b", "c"]: + - result[k] = k +229 + # comment +230 + result = {k: k for k in ["a", "b", "c"]} note: This is an unsafe fix and may change runtime behavior From 69279a5e0009e18cbf52b98c3c3938228a450c26 Mon Sep 17 00:00:00 2001 From: Gaetan Fine <98521677+getehen@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:05:42 +0100 Subject: [PATCH 233/261] [ruff] Fix `--add-noqa` breaking shebangs (#23577) Co-authored-by: Amethyst Reese --- crates/ruff/tests/cli/lint.rs | 82 +++++++++++++++++++ crates/ruff_linter/src/directives.rs | 24 +++++- crates/ruff_linter/src/linter.rs | 24 ++++++ ...nter__tests__shebang_noqa_on_line_one.snap | 5 ++ ...er__tests__shebang_noqa_on_line_three.snap | 5 ++ ...nter__tests__shebang_noqa_on_line_two.snap | 4 + 6 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_one.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_three.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_two.snap diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 12b697a5ddbdc..b5167ef9fd0af 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -1868,6 +1868,88 @@ print( Ok(()) } +#[test] +fn add_noqa_top_of_file() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["D100"] +"#, + )?; + + fixture.write_file( + "noqa.py", r" +", + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .arg("--preview") + .args(["--add-noqa"]) + , @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 1 noqa directive. + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @" # noqa: D100"); + + Ok(()) +} + +#[test] +fn add_noqa_top_of_file_with_shebang() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["D100"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r"#!/usr/bin/env fake command +", + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .arg("--preview") + .args(["--add-noqa"]) + , @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 1 noqa directive. + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @" + #!/usr/bin/env fake command + # noqa: D100 + "); + + Ok(()) +} + #[test] fn add_noqa_exclude() -> Result<()> { let fixture = CliTest::new()?; diff --git a/crates/ruff_linter/src/directives.rs b/crates/ruff_linter/src/directives.rs index ea63f6edfdf4e..5bc0485087adf 100644 --- a/crates/ruff_linter/src/directives.rs +++ b/crates/ruff_linter/src/directives.rs @@ -12,6 +12,7 @@ use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::Locator; +use crate::comments::shebang::ShebangDirective; use crate::noqa::NoqaMapping; use crate::settings::LinterSettings; @@ -124,6 +125,17 @@ fn extract_noqa_line_for(tokens: &Tokens, locator: &Locator, indexer: &Indexer) } } + let mut shebang_mapping = None; + if let Some(first_token) = tokens.first() + && first_token.kind() == TokenKind::Comment + && ShebangDirective::try_extract(locator.slice(first_token)).is_some() + { + shebang_mapping = Some(TextRange::new( + first_token.start(), + locator.full_line_end(first_token.end()), + )); + } + // The capacity allocated here might be more than we need if there are // nested interpolated strings. let mut interpolated_string_mappings = @@ -173,18 +185,24 @@ fn extract_noqa_line_for(tokens: &Tokens, locator: &Locator, indexer: &Indexer) // Merge the mappings in sorted order let mut mappings = NoqaMapping::with_capacity( - continuation_mappings.len() + string_mappings.len() + interpolated_string_mappings.len(), + continuation_mappings.len() + + string_mappings.len() + + interpolated_string_mappings.len() + + usize::from(shebang_mapping.is_some()), ); - let string_mappings = SortedMergeIter { + let all_mappings = SortedMergeIter { left: interpolated_string_mappings.into_iter().peekable(), right: string_mappings.into_iter().peekable(), }; let all_mappings = SortedMergeIter { - left: string_mappings.peekable(), + left: all_mappings.peekable(), right: continuation_mappings.into_iter().peekable(), }; + if let Some(mapping) = shebang_mapping { + mappings.push_mapping(mapping); + } for mapping in all_mappings { mappings.push_mapping(mapping); } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 42b7cd25f0bde..8c40f7b31028a 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1225,4 +1225,28 @@ mod tests { .0; assert_diagnostics!(snapshot, diagnostics); } + + #[test_case( + "on_line_one", + r#"#!/usr/bin/env python #noqa:D100 +"# + )] + #[test_case( + "on_line_two", + r#"#!/usr/bin/env python +#noqa: D100 +"# + )] + #[test_case( + "on_line_three", + r#"#!/usr/bin/env python + +#noqa: D100"# + )] + fn test_shebang_noqa(name: &str, contents: &str) { + let snapshot = format!("shebang_noqa_{name}"); + let settings = LinterSettings::for_rule(Rule::UndocumentedPublicModule); + let diagnostics = test_snippet(contents, &settings); + assert_diagnostics!(snapshot, diagnostics); + } } diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_one.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_one.snap new file mode 100644 index 0000000000000..aa468675d5a59 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_one.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +D100 Missing docstring in public module +--> :1:1 diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_three.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_three.snap new file mode 100644 index 0000000000000..aa468675d5a59 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_three.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +D100 Missing docstring in public module +--> :1:1 diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_two.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_two.snap new file mode 100644 index 0000000000000..4ba33c756c494 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__shebang_noqa_on_line_two.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + From 0b05e455880b3bc7c336f0559261209a144a7c81 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 6 Mar 2026 15:28:46 -0500 Subject: [PATCH 234/261] Avoid syntax errors in RUF036 fixes (#23764) ## Summary Part of: https://github.com/astral-sh/ruff/issues/23763. --- .../resources/test/fixtures/ruff/RUF036.py | 6 ++- .../ruff/rules/none_not_at_end_of_union.rs | 10 ++++- ..._rules__ruff__tests__RUF036_RUF036.py.snap | 38 ++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py index 62d00b006509e..78c6e136343ff 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py @@ -66,6 +66,11 @@ def func14(arg: None | int | str | bytes): ... +# Preserve token boundaries when fixing non-annotation expressions. +print(None | (int)and 2) +print(2 or(None) | int) + + # Ok def good_func1(arg: int | None): ... @@ -85,4 +90,3 @@ def good_func4(arg: U[None]): def good_func5(arg: U[int]): ... - diff --git a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs index 564570897ae0c..7ffb240174de6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs @@ -7,6 +7,7 @@ use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::fix::edits::pad; use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does @@ -184,6 +185,13 @@ fn generate_fix( }) }; - let edit = Edit::range_replacement(checker.generator().expr(&new_expr), annotation.range()); + let edit = Edit::range_replacement( + pad( + checker.generator().expr(&new_expr), + annotation.range(), + checker.locator(), + ), + annotation.range(), + ); Some(Fix::applicable_edit(edit, applicability)) } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap index fc03fc791a7eb..f0519f35182ff 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap @@ -238,4 +238,40 @@ help: Move `None` to the end of the type union 65 + def func14(arg: int | str | bytes | None): 66 | ... 67 | -68 | +68 | + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:70:7 + | +69 | # Preserve token boundaries when fixing non-annotation expressions. +70 | print(None | (int)and 2) + | ^^^^^^^^^^^^ +71 | print(2 or(None) | int) + | +help: Move `None` to the end of the type union +67 | +68 | +69 | # Preserve token boundaries when fixing non-annotation expressions. + - print(None | (int)and 2) +70 + print(int | None and 2) +71 | print(2 or(None) | int) +72 | +73 | + +RUF036 [*] `None` not at the end of the type union. + --> RUF036.py:71:11 + | +69 | # Preserve token boundaries when fixing non-annotation expressions. +70 | print(None | (int)and 2) +71 | print(2 or(None) | int) + | ^^^^^^^^^^^^ + | +help: Move `None` to the end of the type union +68 | +69 | # Preserve token boundaries when fixing non-annotation expressions. +70 | print(None | (int)and 2) + - print(2 or(None) | int) +71 + print(2 or int | None) +72 | +73 | +74 | # Ok From 724d3b702e31c16d0695f281f5e988050b15b296 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 6 Mar 2026 15:33:19 -0500 Subject: [PATCH 235/261] Limit RUF036 to typing contexts; make it unsafe for non-typing-only (#23765) ## Summary Closes https://github.com/astral-sh/ruff/issues/23763. --- .../resources/test/fixtures/ruff/RUF036.py | 10 +++-- crates/ruff_linter/src/rules/ruff/mod.rs | 19 ++++++++- .../ruff/rules/none_not_at_end_of_union.rs | 13 +++++-- ...tests__PY313_RUF036_runtime_evaluated.snap | 18 +++++++++ ..._rules__ruff__tests__RUF036_RUF036.py.snap | 39 +++++++++---------- 5 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY313_RUF036_runtime_evaluated.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py index 78c6e136343ff..58712a3b4f590 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py @@ -66,9 +66,13 @@ def func14(arg: None | int | str | bytes): ... -# Preserve token boundaries when fixing non-annotation expressions. -print(None | (int)and 2) -print(2 or(None) | int) +# Preserve token boundaries when fixing annotations. +def func15(arg: None | (int)and 2): + ... + + +def func16(arg: 2 or(None) | int): + ... # Ok diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 939067fd24d41..e62f967acd92a 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -22,7 +22,7 @@ mod tests { use crate::rules::pydocstyle::settings::Settings as PydocstyleSettings; use crate::settings::LinterSettings; use crate::settings::types::{CompiledPerFileIgnoreList, PerFileIgnore, PreviewMode}; - use crate::test::{test_path, test_resource_path}; + use crate::test::{test_path, test_resource_path, test_snippet}; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] @@ -228,6 +228,23 @@ mod tests { Ok(()) } + #[test] + fn none_not_at_end_of_union_py313() { + let diagnostics = test_snippet( + r" + def func(arg: None | int): + ... + + print(None | (int)and 2) + ", + &settings::LinterSettings { + unresolved_target_version: PythonVersion::PY313.into(), + ..settings::LinterSettings::for_rule(Rule::NoneNotAtEndOfUnion) + }, + ); + assert_diagnostics!("PY313_RUF036_runtime_evaluated", diagnostics); + } + #[test] fn access_annotations_from_class_dict_py310() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs index 7ffb240174de6..a58c187987fcb 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs @@ -91,6 +91,11 @@ fn is_union_expr(semantic: &SemanticModel, expr: &Expr) -> bool { /// RUF036 pub(crate) fn none_not_at_end_of_union<'a>(checker: &Checker, union: &'a Expr) { let semantic = checker.semantic(); + + if !semantic.in_type_definition() { + return; + } + let mut none_exprs: Vec<&Expr> = Vec::new(); let mut other_exprs: Vec<&Expr> = Vec::new(); @@ -150,10 +155,12 @@ fn generate_fix( annotation: &Expr, is_pep604: bool, ) -> Option { - let applicability = if checker.comment_ranges().intersects(annotation.range()) { - Applicability::Unsafe - } else { + let applicability = if checker.semantic().in_typing_only_annotation() + && !checker.comment_ranges().intersects(annotation.range()) + { Applicability::Safe + } else { + Applicability::Unsafe }; let reordered: Vec = other_exprs diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY313_RUF036_runtime_evaluated.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY313_RUF036_runtime_evaluated.snap new file mode 100644 index 0000000000000..83de47888b65f --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY313_RUF036_runtime_evaluated.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF036 [*] `None` not at the end of the type union. + --> :2:15 + | +2 | def func(arg: None | int): + | ^^^^^^^^^^ +3 | ... + | +help: Move `None` to the end of the type union +1 | + - def func(arg: None | int): +2 + def func(arg: int | None): +3 | ... +4 | +5 | print(None | (int)and 2) +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap index f0519f35182ff..7a3adee017cd5 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap @@ -241,37 +241,36 @@ help: Move `None` to the end of the type union 68 | RUF036 [*] `None` not at the end of the type union. - --> RUF036.py:70:7 + --> RUF036.py:70:17 | -69 | # Preserve token boundaries when fixing non-annotation expressions. -70 | print(None | (int)and 2) - | ^^^^^^^^^^^^ -71 | print(2 or(None) | int) +69 | # Preserve token boundaries when fixing annotations. +70 | def func15(arg: None | (int)and 2): + | ^^^^^^^^^^^^ +71 | ... | help: Move `None` to the end of the type union 67 | 68 | -69 | # Preserve token boundaries when fixing non-annotation expressions. - - print(None | (int)and 2) -70 + print(int | None and 2) -71 | print(2 or(None) | int) +69 | # Preserve token boundaries when fixing annotations. + - def func15(arg: None | (int)and 2): +70 + def func15(arg: int | None and 2): +71 | ... 72 | 73 | RUF036 [*] `None` not at the end of the type union. - --> RUF036.py:71:11 + --> RUF036.py:74:21 | -69 | # Preserve token boundaries when fixing non-annotation expressions. -70 | print(None | (int)and 2) -71 | print(2 or(None) | int) - | ^^^^^^^^^^^^ +74 | def func16(arg: 2 or(None) | int): + | ^^^^^^^^^^^^ +75 | ... | help: Move `None` to the end of the type union -68 | -69 | # Preserve token boundaries when fixing non-annotation expressions. -70 | print(None | (int)and 2) - - print(2 or(None) | int) -71 + print(2 or int | None) +71 | ... 72 | 73 | -74 | # Ok + - def func16(arg: 2 or(None) | int): +74 + def func16(arg: 2 or int | None): +75 | ... +76 | +77 | From a35500364a0cbd1a6c60cf2fd49f903d7c5046f5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 6 Mar 2026 17:23:39 -0500 Subject: [PATCH 236/261] [ty] Fix release version for `duplicate-kw-only` (#23769) ## Summary This rule was introduced in [0.0.15](https://github.com/astral-sh/ty/releases/tag/0.0.15). --- crates/ty/docs/rules.md | 2 +- crates/ty_python_semantic/src/types/diagnostic.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index ad0e4e0a9ff9f..0eaf934e77651 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -414,7 +414,7 @@ type B = A Default level: error · -Preview (since 1.0.0) · +Added in 0.0.15 · Related issues · View source diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 1209d9a8c808b..76d27ce353f4c 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -477,7 +477,7 @@ declare_lint! { /// ``` pub(crate) static DATACLASS_FIELD_ORDER = { summary: "detects dataclass definitions with required fields after fields with default values", - status: LintStatus::preview("1.0.0"), + status: LintStatus::stable("0.0.15"), default_level: Level::Error, } } From 2185bec02b3c7e9e16cc2572e49ecc065c15c875 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 6 Mar 2026 23:15:37 +0000 Subject: [PATCH 237/261] [ty] Fixup restoration of multi-inference state in `type_expression.rs` (#23760) ## Summary Two small followups to d9daf6d430623a8a5dd25cb52a3b6f2af961b9cb: - A minor fix for a bug I introduced in that commit. We should reset `context.multi_inference` to the state it was previously in rather than hardcoding `false` here. This has real-world impact on `altair` (see the mypy_primer report) - Also detect invalid PEP-604 unions involving `types.GenericAlias` instances and objects that don't have `__or__` methods at runtime. These need to be special-cased in the same way as class-literal types, because of typeshed's very forgiving `types.GenericAlias.__or__` annotation: https://github.com/python/typeshed/blob/07ffb67b924d5f532f8b8b72a4902f58faae6aca/stdlib/types.pyi#L700-L702 ## Test Plan mdtests extended --- .../resources/mdtest/annotations/string.md | 3 + ...n_3.1\342\200\246_(5e6477d05ddea33f).snap" | 149 ++++++++++++------ .../types/infer/builder/type_expression.rs | 37 +++-- 3 files changed, 123 insertions(+), 66 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index ffa1df89bef23..ca1994513d090 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -103,6 +103,9 @@ def f( # error: [unresolved-reference] "SomethingUndefined" # error: [unresolved-reference] "SomethingAlsoUndefined" i: SomethingUndefined | SomethingAlsoUndefined, + # error: [unsupported-operator] + # error: [unsupported-operator] + j: list["int" | None] | "bytes", ): reveal_type(a) # revealed: int | Foo reveal_type(b) # revealed: int | memoryview[int] | bytes diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" index bb45195da772e..ca2eb92bea212 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" @@ -49,38 +49,41 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/string.md 34 | # error: [unresolved-reference] "SomethingUndefined" 35 | # error: [unresolved-reference] "SomethingAlsoUndefined" 36 | i: SomethingUndefined | SomethingAlsoUndefined, -37 | ): -38 | reveal_type(a) # revealed: int | Foo -39 | reveal_type(b) # revealed: int | memoryview[int] | bytes -40 | reveal_type(c) # revealed: TD | None -41 | reveal_type(d) # revealed: P | None -42 | reveal_type(e) # revealed: T@f | Foo -43 | reveal_type(f) # revealed: Foo | ((...) -> None) -44 | reveal_type(g) # revealed: UsesMeta | Foo -45 | reveal_type(h) # revealed: None -46 | reveal_type(i) # revealed: Unknown -47 | -48 | # fmt: on -49 | -50 | class Foo: ... -51 | -52 | # error: [unsupported-operator] -53 | X = list["int" | None] +37 | # error: [unsupported-operator] +38 | # error: [unsupported-operator] +39 | j: list["int" | None] | "bytes", +40 | ): +41 | reveal_type(a) # revealed: int | Foo +42 | reveal_type(b) # revealed: int | memoryview[int] | bytes +43 | reveal_type(c) # revealed: TD | None +44 | reveal_type(d) # revealed: P | None +45 | reveal_type(e) # revealed: T@f | Foo +46 | reveal_type(f) # revealed: Foo | ((...) -> None) +47 | reveal_type(g) # revealed: UsesMeta | Foo +48 | reveal_type(h) # revealed: None +49 | reveal_type(i) # revealed: Unknown +50 | +51 | # fmt: on +52 | +53 | class Foo: ... 54 | -55 | if TYPE_CHECKING: -56 | # TODO: ideally we would not error here, since `if TYPE_CHECKING` -57 | # blocks are not executed at runtime. Requires -58 | # https://github.com/astral-sh/ty/issues/1553. -59 | bar: "int" | "None" # error: [unsupported-operator] -60 | -61 | # TODO: same as above -62 | # error: [unsupported-operator] -63 | def foo(x: "int" | "None"): ... -64 | -65 | class Bar: -66 | # no error because this annotation is resolved inside a scope -67 | # fully defined inside an `if TYPE_CHECKING` block -68 | def f(x: "int" | "None"): ... +55 | # error: [unsupported-operator] +56 | X = list["int" | None] +57 | +58 | if TYPE_CHECKING: +59 | # TODO: ideally we would not error here, since `if TYPE_CHECKING` +60 | # blocks are not executed at runtime. Requires +61 | # https://github.com/astral-sh/ty/issues/1553. +62 | bar: "int" | "None" # error: [unsupported-operator] +63 | +64 | # TODO: same as above +65 | # error: [unsupported-operator] +66 | def foo(x: "int" | "None"): ... +67 | +68 | class Bar: +69 | # no error because this annotation is resolved inside a scope +70 | # fully defined inside an `if TYPE_CHECKING` block +71 | def f(x: "int" | "None"): ... ``` # Diagnostics @@ -194,8 +197,8 @@ error[unresolved-reference]: Name `SomethingUndefined` used when not defined 35 | # error: [unresolved-reference] "SomethingAlsoUndefined" 36 | i: SomethingUndefined | SomethingAlsoUndefined, | ^^^^^^^^^^^^^^^^^^ -37 | ): -38 | reveal_type(a) # revealed: int | Foo +37 | # error: [unsupported-operator] +38 | # error: [unsupported-operator] | info: rule `unresolved-reference` is enabled by default @@ -209,8 +212,8 @@ error[unresolved-reference]: Name `SomethingAlsoUndefined` used when not defined 35 | # error: [unresolved-reference] "SomethingAlsoUndefined" 36 | i: SomethingUndefined | SomethingAlsoUndefined, | ^^^^^^^^^^^^^^^^^^^^^^ -37 | ): -38 | reveal_type(a) # revealed: int | Foo +37 | # error: [unsupported-operator] +38 | # error: [unsupported-operator] | info: rule `unresolved-reference` is enabled by default @@ -218,16 +221,58 @@ info: rule `unresolved-reference` is enabled by default ``` error[unsupported-operator]: Unsupported `|` operation - --> src/mdtest_snippet.py:53:10 + --> src/mdtest_snippet.py:39:8 | -52 | # error: [unsupported-operator] -53 | X = list["int" | None] +37 | # error: [unsupported-operator] +38 | # error: [unsupported-operator] +39 | j: list["int" | None] | "bytes", + | ------------------^^^------- + | | | + | | Has type `Literal["bytes"]` + | Has type `` +40 | ): +41 | reveal_type(a) # revealed: int | Foo + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:39:13 + | +37 | # error: [unsupported-operator] +38 | # error: [unsupported-operator] +39 | j: list["int" | None] | "bytes", + | -----^^^---- + | | | + | | Has type `None` + | Has type `Literal["int"]` +40 | ): +41 | reveal_type(a) # revealed: int | Foo + | +info: All type expressions are evaluated at runtime by default on Python <3.14 +info: Python 3.13 was assumed when inferring types because it was specified on the command line +help: Put quotes around the whole union rather than just certain elements +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `|` operation + --> src/mdtest_snippet.py:56:10 + | +55 | # error: [unsupported-operator] +56 | X = list["int" | None] | -----^^^---- | | | | | Has type `None` | Has type `Literal["int"]` -54 | -55 | if TYPE_CHECKING: +57 | +58 | if TYPE_CHECKING: | info: All type expressions are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line @@ -238,17 +283,17 @@ info: rule `unsupported-operator` is enabled by default ``` error[unsupported-operator]: Unsupported `|` operation - --> src/mdtest_snippet.py:59:10 + --> src/mdtest_snippet.py:62:10 | -57 | # blocks are not executed at runtime. Requires -58 | # https://github.com/astral-sh/ty/issues/1553. -59 | bar: "int" | "None" # error: [unsupported-operator] +60 | # blocks are not executed at runtime. Requires +61 | # https://github.com/astral-sh/ty/issues/1553. +62 | bar: "int" | "None" # error: [unsupported-operator] | -----^^^------ | | | | | Has type `Literal["None"]` | Has type `Literal["int"]` -60 | -61 | # TODO: same as above +63 | +64 | # TODO: same as above | info: All type expressions are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line @@ -259,17 +304,17 @@ info: rule `unsupported-operator` is enabled by default ``` error[unsupported-operator]: Unsupported `|` operation - --> src/mdtest_snippet.py:63:16 + --> src/mdtest_snippet.py:66:16 | -61 | # TODO: same as above -62 | # error: [unsupported-operator] -63 | def foo(x: "int" | "None"): ... +64 | # TODO: same as above +65 | # error: [unsupported-operator] +66 | def foo(x: "int" | "None"): ... | -----^^^------ | | | | | Has type `Literal["None"]` | Has type `Literal["int"]` -64 | -65 | class Bar: +67 | +68 | class Bar: | info: All type expressions are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index d5f653d06686a..c661519ee4944 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -162,7 +162,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let previous_state = self.set_multi_inference_state(MultiInferenceState::Ignore); - self.context.set_multi_inference(true); + let was_in_multi_inference = self.context.set_multi_inference(true); // If the left-hand side of the union is itself a PEP-604 union, // we'll already have checked whether it can be used with `|` in a previous inference step // and emitted a diagnostic if it was appropriate. We should skip inferring it here to @@ -173,7 +173,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let right_type_value = self.infer_expression(&binary.right, TypeContext::default()); self.multi_inference_state = previous_state; - self.context.set_multi_inference(false); + self.context.set_multi_inference(was_in_multi_inference); let dunder_fails = Type::try_call_bin_op( self.db(), @@ -185,18 +185,27 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // As well as trying the normal dunder lookup, // we also check for the case where one of the operands is a class-literal type - // and the other is a string literal. The normal dunder lookup fails to catch - // this error, since typeshed annotates `type.__(r)or__` as accepting `Any`. - let should_emit_error = dunder_fails - || matches!( - (left_type_value, right_type_value), - ( - Type::ClassLiteral(class), Type::LiteralValue(literal)) - | (Type::LiteralValue(literal), Type::ClassLiteral(class) - ) - if class.metaclass(self.db()) == KnownClass::Type.to_class_literal(self.db()) - && !literal.is_enum() - ); + // or generic-alias type and the other is a string literal. The normal dunder lookup + // fails to catch this error, since typeshed annotates `type.__(r)or__` as accepting `Any`. + let should_emit_error = if dunder_fails { + true + } else { + let literal = match (left_type_value, right_type_value) { + (Type::ClassLiteral(class), Type::LiteralValue(literal)) + | (Type::LiteralValue(literal), Type::ClassLiteral(class)) + if class.metaclass(self.db()) + == KnownClass::Type.to_class_literal(self.db()) => + { + Some(literal) + } + (Type::GenericAlias(_), Type::LiteralValue(literal)) + | (Type::LiteralValue(literal), Type::GenericAlias(_)) => { + Some(literal) + } + _ => None, + }; + literal.is_some_and(|literal| !literal.is_enum()) + }; if should_emit_error && let Some(builder) = From f8df8eadc063fca0b3650c7a6fc1ea10b87bddea Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 6 Mar 2026 21:59:35 -0500 Subject: [PATCH 238/261] [ty] Support enum member access through enum instances and members (#23772) ## Summary Closes https://github.com/astral-sh/ty/issues/2977. --- .../resources/mdtest/enums.md | 19 +++++++++++++++- .../mdtest/exhaustiveness_checking.md | 12 ++++++++++ crates/ty_python_semantic/src/types.rs | 22 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 112cf380dea7a..15f63f63e6090 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -916,7 +916,8 @@ class Answer(Enum): def is_yes(self) -> bool: return self == Answer.YES - constant: int = 1 # error: [invalid-enum-member-annotation] + + constant: int reveal_type(Answer.YES.is_yes()) # revealed: bool reveal_type(Answer.YES.constant) # revealed: int @@ -932,6 +933,22 @@ class MyAnswer(MyEnum): reveal_type(MyAnswer.YES.some_method()) # revealed: None ``` +## Accessing enum members from enum members / instances + +```py +from enum import Enum + +class Answer(Enum): + YES = 1 + NO = 2 + +reveal_type(Answer.YES.NO) # revealed: Literal[Answer.NO] + +def _(answer: Answer) -> None: + reveal_type(answer.YES) # revealed: Literal[Answer.YES] + reveal_type(answer.NO) # revealed: Literal[Answer.NO] +``` + ## Accessing enum members from `type[…]` ```py diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 4b7137ef98b76..0b95194737ccc 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -167,6 +167,18 @@ def match_exhaustive_no_assertion(x: Color) -> int: case Color.BLUE: return 3 +def match_exhaustive_through_instance(x: Color) -> int: + match x: + case x.RED: + reveal_type(x) # revealed: Literal[Color.RED] + return 1 + case x.GREEN: + reveal_type(x) # revealed: Literal[Color.GREEN] + return 2 + case x.BLUE: + reveal_type(x) # revealed: Literal[Color.BLUE] + return 3 + def match_non_exhaustive(x: Color): match x: case Color.RED: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f7913e7715379..3305bfcd1dda2 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -3199,6 +3199,28 @@ impl<'db> Type<'db> { | Type::TypeIs(..) | Type::TypeGuard(..) | Type::TypedDict(_) => { + // Enum members can be accessed through enum instances and other enum members, + // e.g. `answer.YES` or `Answer.YES.NO`. + let enum_class = match self { + Type::LiteralValue(literal) => literal + .as_enum() + .map(|enum_literal| enum_literal.enum_class(db)), + Type::NominalInstance(instance) => Some(instance.class_literal(db)), + _ => None, + }; + + if let Some(enum_class) = enum_class + && let Some(metadata) = enum_metadata(db, enum_class) + && let Some(resolved_name) = metadata.resolve_member(&name) + { + return Place::bound(Type::enum_literal(EnumLiteralType::new( + db, + enum_class, + resolved_name.clone(), + ))) + .into(); + } + let fallback = self.instance_member(db, name_str); let result = self.invoke_descriptor_protocol( From edac24bbd1b22ab7bc050512d89aa8c67818c428 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 7 Mar 2026 15:47:55 +0000 Subject: [PATCH 239/261] [ty] Don't promote module-literal types (#23786) --- .../resources/mdtest/promotion.md | 23 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 17 +------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/promotion.md b/crates/ty_python_semantic/resources/mdtest/promotion.md index 1dd0593822987..c0987d7613234 100644 --- a/crates/ty_python_semantic/resources/mdtest/promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/promotion.md @@ -501,3 +501,26 @@ def _(a: A | None): reveal_type(d) # revealed: dict[str, A] return {} ``` + +## Module-literal types are not promoted + +Since module-literal types are "literal" types in a certain sense (each type is a singleton type), +we used to promote module-literal types to `types.ModuleType`. We no longer do, because +`types.ModuleType` is a very broad type that is not particularly useful. The fake +`types.ModuleType.__getattr__` method that typeshed provides also meant that you would not receive +any errors from clearly incorrect code like this: + +`module1.py`: + +```py +``` + +`main.py`: + +```py +import module1 + +my_modules = [module1] +reveal_type(my_modules) # revealed: list[] +my_modules[0].flibbertigibbet # error: [unresolved-attribute] +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3305bfcd1dda2..79c59608cd147 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1706,7 +1706,6 @@ impl<'db> Type<'db> { fn promote_impl(self, db: &'db dyn Db) -> Type<'db> { match self { Type::LiteralValue(literal) if literal.is_promotable() => literal.fallback_instance(db), - Type::ModuleLiteral(_) => KnownClass::ModuleType.to_instance(db), Type::FunctionLiteral(literal) => Type::Callable(literal.into_callable_type(db)), _ => self, } @@ -5396,21 +5395,6 @@ impl<'db> Type<'db> { } } - Type::ModuleLiteral(_) => match type_mapping { - TypeMapping::ApplySpecialization(_) | - TypeMapping::ApplySpecializationWithMaterialization { .. } | - TypeMapping::UniqueSpecialization { .. } | - TypeMapping::BindLegacyTypevars(_) | - TypeMapping::BindSelf(..) | - TypeMapping::ReplaceSelf { .. } | - TypeMapping::Materialize(_) | - TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion | - TypeMapping::RescopeReturnCallables(_) | - TypeMapping::Promote(PromotionMode::Off) => self, - TypeMapping::Promote(PromotionMode::On) => self.promote_impl(db) - } - Type::LiteralValue(_) => match type_mapping { TypeMapping::ApplySpecialization(_) | TypeMapping::ApplySpecializationWithMaterialization { .. } | @@ -5447,6 +5431,7 @@ impl<'db> Type<'db> { | Type::AlwaysTruthy | Type::AlwaysFalsy | Type::WrapperDescriptor(_) + | Type::ModuleLiteral(_) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::ConstraintSetRange From 5cdd2fe4356648a2cab79e5e9f466aa66cc265a7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 7 Mar 2026 17:54:21 +0000 Subject: [PATCH 240/261] More minor improvements to `conformance.py` (#23792) --- scripts/conformance.py | 96 ++++++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 32 deletions(-) diff --git a/scripts/conformance.py b/scripts/conformance.py index 9629ef5144caf..ff92121788f43 100644 --- a/scripts/conformance.py +++ b/scripts/conformance.py @@ -76,13 +76,12 @@ GITHUB_HEADER = [ "", - "", "", "", "", "", ] -GITHUB_FOOTER = ["", "
Test caseDiff
"] +GITHUB_FOOTER = [""] SUMMARY_NOTE = """ Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics @@ -370,7 +369,6 @@ def render_html_diff_row(tc: TestCase, *, source: Source | None) -> list[str]: return [ "", "", - "", "", "", location, @@ -384,7 +382,6 @@ def render_html_diff_row(tc: TestCase, *, source: Source | None) -> list[str]: "```", "", "", - "", "", ] @@ -719,16 +716,28 @@ def render_test_cases( lines.append("```") else: lines.extend(GITHUB_FOOTER) - lines.extend(["", "", ""]) + lines.extend(["", ""]) return "\n".join(lines) -def render_file_stats_table(test_cases: list[TestCase]) -> str: - """Render a per-file breakdown showing only files whose TP/FP/FN counts changed.""" +def collect_file_stats(test_cases: list[TestCase]) -> list[FileStats]: + """Compute per-file statistics from grouped test cases.""" path_to_cases: dict[Path, list[TestCase]] = {} for tc in test_cases: path_to_cases.setdefault(tc.path, []).append(tc) + return [ + FileStats( + path=path, + old=compute_stats(cases, Source.OLD), + new=compute_stats(cases, Source.NEW), + ) + for path, cases in path_to_cases.items() + ] + + +def render_file_stats_table(file_stats: list[FileStats]) -> str: + """Render a per-file breakdown showing only files whose TP/FP/FN counts changed.""" def fmt(old: int, new: int, *, greater_is_better: bool = True) -> str: if old == new: @@ -738,19 +747,13 @@ def fmt(old: int, new: int, *, greater_is_better: bool = True) -> str: indicator = " ✅" if improved else " ❌" return f"{new} ({diff:+}){indicator}" - # Collect per-file data; track totals across all files regardless of change. - file_stats: list[FileStats] = [] + # Collect totals across all files regardless of change. old_totals = Statistics() new_totals = Statistics() passing = 0 - total_files = 0 + total_files = len(file_stats) - for path, cases in path_to_cases.items(): - fs = FileStats( - path=path, - old=compute_stats(cases, Source.OLD), - new=compute_stats(cases, Source.NEW), - ) + for fs in file_stats: old_totals.true_positives += fs.old.true_positives old_totals.false_positives += fs.old.false_positives old_totals.false_negatives += fs.old.false_negatives @@ -758,8 +761,6 @@ def fmt(old: int, new: int, *, greater_is_better: bool = True) -> str: new_totals.false_positives += fs.new.false_positives new_totals.false_negatives += fs.new.false_negatives passing += fs.new_passes - total_files += 1 - file_stats.append(fs) changed = [fs for fs in file_stats if fs.total_change > 0] if not changed: @@ -772,9 +773,9 @@ def fmt(old: int, new: int, *, greater_is_better: bool = True) -> str: if fs.new_passes and not fs.old_passes: status = "✅ Newly Passing 🎉" elif fs.old_passes and not fs.new_passes: - status = "❌ Newly Failing" + status = "❌ Newly Failing ☹️" elif fs.new_passes: - status = "✅" + status = "✅ Still Passing" else: old_errors = fs.old.false_positives + fs.old.false_negatives new_errors = fs.new.false_positives + fs.new.false_negatives @@ -849,17 +850,34 @@ def diff_format( assert_never((greater_is_better, increased)) # ty: ignore[type-assertion-failure] -def render_summary(test_cases: list[TestCase], *, force_summary_table: bool) -> str: - def format_metric(diff: float, old: float, new: float): +def render_summary( + test_cases: list[TestCase], + file_stats: list[FileStats], + *, + force_summary_table: bool, +) -> str: + def format_metric(diff: float, old: float, new: float) -> str: + if diff > 0: + return f"increased from {old:.2%} to {new:.2%}" + if diff < 0: + return f"decreased from {old:.2%} to {new:.2%}" + return f"held steady at {old:.2%}" + + def format_int_metric(diff: int, old: int, new: int, total: int) -> str: if diff > 0: - return f"increased from {old:.2%} to {new:.2%}" + return f"improved from {old}/{total} to {new}/{total}" if diff < 0: - return f"decreased from {old:.2%} to {new:.2%}" - return f"held steady at {old:.2%}" + return f"regressed from {old}/{total} to {new}/{total}" + return f"held steady at {old}/{total}" old = compute_stats(test_cases, Source.OLD) new = compute_stats(test_cases, Source.NEW) + old_files_passing = sum(fs.old_passes for fs in file_stats) + new_files_passing = sum(fs.new_passes for fs in file_stats) + total_files = len(file_stats) + files_passing_change = new_files_passing - old_files_passing + assert new.true_positives > 0, ( "Expected ty to have at least one true positive.\n" f"Sample of grouped diagnostics: {test_cases[:5]}" @@ -876,7 +894,9 @@ def format_metric(diff: float, old: float, new: float): f"The percentage of diagnostics emitted that were expected errors " f"{format_metric(precision_change, old.precision, new.precision)}. " f"The percentage of expected errors that received a diagnostic " - f"{format_metric(recall_change, old.recall, new.recall)}." + f"{format_metric(recall_change, old.recall, new.recall)}. " + f"The number of fully passing files " + f"{format_int_metric(files_passing_change, old_files_passing, new_files_passing, total_files)}." ) base_header = f"[Typing conformance results]({CONFORMANCE_DIR_WITH_README})" @@ -906,13 +926,18 @@ def format_metric(diff: float, old: float, new: float): precision_diff = diff_format(precision_change, greater_is_better=True) recall_diff = diff_format(recall_change, greater_is_better=True) total_diff = diff_format(total_change, neutral=True) + passing_diff = diff_format(files_passing_change, greater_is_better=True) - if (precision_change > 0 and recall_change >= 0) or ( - recall_change > 0 and precision_change >= 0 + if ( + (precision_change > 0 and recall_change >= 0 and files_passing_change >= 0) + or (recall_change > 0 and precision_change >= 0 and files_passing_change >= 0) + or (files_passing_change > 0 and precision_change >= 0 and recall_change >= 0) ): header = f"{base_header} improved 🎉" - elif (precision_change < 0 and recall_change <= 0) or ( - recall_change < 0 and precision_change <= 0 + elif ( + (precision_change < 0 and recall_change <= 0 and files_passing_change <= 0) + or (recall_change < 0 and precision_change <= 0 and files_passing_change <= 0) + or (files_passing_change < 0 and precision_change <= 0 and recall_change <= 0) ): header = f"{base_header} regressed ❌" else: @@ -945,6 +970,7 @@ def format_metric(diff: float, old: float, new: float): | Total Diagnostics | {old.total_diagnostics} | {new.total_diagnostics} | {total_change:+} | {total_diff} | | Precision | {old.precision:.2%} | {new.precision:.2%} | {precision_change:+.2%} | {precision_diff} | | Recall | {old.recall:.2%} | {new.recall:.2%} | {recall_change:+.2%} | {recall_diff} | + | Passing Files | {old_files_passing}/{total_files} | {new_files_passing}/{total_files} | {files_passing_change:+} | {passing_diff} | """ ) @@ -1060,12 +1086,18 @@ def main(): expected=expected, ) + file_stats = collect_file_stats(grouped) + rendered = "\n\n".join( filter( None, [ - render_summary(grouped, force_summary_table=args.force_summary_table), - render_file_stats_table(grouped), + render_summary( + grouped, + file_stats, + force_summary_table=args.force_summary_table, + ), + render_file_stats_table(file_stats), render_test_cases(grouped, format=args.format), ], ) From 359981b1e8ea977c53b934517ed7a7d7c15c5e92 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 7 Mar 2026 10:18:03 -0800 Subject: [PATCH 241/261] CLAUDE.md -> AGENTS.md (#23791) --- AGENTS.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 94 +------------------------------------------------------ 2 files changed, 94 insertions(+), 93 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000000..a42126a8653ff --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,93 @@ +# Ruff Repository + +This repository contains both Ruff (a Python linter and formatter) and ty (a Python type checker). The crates follow a naming convention: `ruff_*` for Ruff-specific code and `ty_*` for ty-specific code. ty reuses several Ruff crates, including the Python parser (`ruff_python_parser`) and AST definitions (`ruff_python_ast`). + +## Running Tests + +Run all tests (using `nextest` for faster execution): + +```sh +cargo nextest run +``` + +For faster test execution, use the `fast-test` profile which enables optimizations while retaining debug info: + +```sh +cargo nextest run --cargo-profile fast-test +``` + +Run tests for a specific crate: + +```sh +cargo nextest run -p ty_python_semantic +``` + +Run a single mdtest file: + +```sh +cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: +``` + +To run a specific mdtest within a file, use a substring of the Markdown header text as `MDTEST_TEST_FILTER`. Only use this if it's necessary to isolate a single test case: + +```sh +MDTEST_TEST_FILTER="" cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: +``` + +Update snapshots after running tests: + +```sh +cargo insta accept +``` + +## Running Clippy + +```sh +cargo clippy --workspace --all-targets --all-features -- -D warnings +``` + +## Running Debug Builds + +Use debug builds (not `--release`) when developing, as release builds lack debug assertions and have slower compile times. + +Run Ruff: + +```sh +cargo run --bin ruff -- check path/to/file.py +``` + +Run ty: + +```sh +cargo run --bin ty -- check path/to/file.py +``` + +## Reproducing ty ecosystem changes + +If asked to reproduce changes in the ty ecosystem, use this script to clone the project to some +directory and install its dependencies into `.venv`: + +```sh +uv run scripts/setup_primer_project.py +``` + +## Pull Requests + +When working on ty, PR titles should start with `[ty]` and be tagged with the `ty` GitHub label. + +## Development Guidelines + +- All changes must be tested. If you're not testing your changes, you're not done. +- Look to see if your tests could go in an existing file before adding a new file for your tests. +- Get your tests to pass. If you didn't run the tests, your code does not work. +- Follow existing code style. Check neighboring files for patterns. +- Rust imports should always go at the top of the file, never locally in functions. +- Always run `uvx prek run -a` at the end of a task, after every rebase, after addressing any review comment, and before pushing any code. +- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first. +- Try hard to avoid patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls. +- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability. At the end of a task, always check your work to see if you missed opportunities to use `let` chains. +- If you *have* to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. But if a lint is complaining about unused/dead code, it's usually best to just delete the unused code. +- Use comments purposefully. Don't use comments to narrate code, but do use them to explain invariants and why something unusual was done a particular way. +- When adding new ty checks, it's important to make error messages concise. Think about how an error message would look on a narrow terminal screen. Sometimes more detail can be provided in subdiagnostics or secondary annotations, but it's also important to make sure that the diagnostic is understandable if the user has passed `--output-format=concise`. +- **Salsa incrementality (ty):** Any method that accesses `.node()` must be `#[salsa::tracked]`, or it will break incrementality. Prefer higher-level semantic APIs over raw AST access. +- Run `cargo dev generate-all` after changing configuration options, CLI arguments, lint rules, or environment variable definitions, as these changes require regeneration of schemas, docs, and CLI references. diff --git a/CLAUDE.md b/CLAUDE.md index a42126a8653ff..43c994c2d3617 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,93 +1 @@ -# Ruff Repository - -This repository contains both Ruff (a Python linter and formatter) and ty (a Python type checker). The crates follow a naming convention: `ruff_*` for Ruff-specific code and `ty_*` for ty-specific code. ty reuses several Ruff crates, including the Python parser (`ruff_python_parser`) and AST definitions (`ruff_python_ast`). - -## Running Tests - -Run all tests (using `nextest` for faster execution): - -```sh -cargo nextest run -``` - -For faster test execution, use the `fast-test` profile which enables optimizations while retaining debug info: - -```sh -cargo nextest run --cargo-profile fast-test -``` - -Run tests for a specific crate: - -```sh -cargo nextest run -p ty_python_semantic -``` - -Run a single mdtest file: - -```sh -cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: -``` - -To run a specific mdtest within a file, use a substring of the Markdown header text as `MDTEST_TEST_FILTER`. Only use this if it's necessary to isolate a single test case: - -```sh -MDTEST_TEST_FILTER="" cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: -``` - -Update snapshots after running tests: - -```sh -cargo insta accept -``` - -## Running Clippy - -```sh -cargo clippy --workspace --all-targets --all-features -- -D warnings -``` - -## Running Debug Builds - -Use debug builds (not `--release`) when developing, as release builds lack debug assertions and have slower compile times. - -Run Ruff: - -```sh -cargo run --bin ruff -- check path/to/file.py -``` - -Run ty: - -```sh -cargo run --bin ty -- check path/to/file.py -``` - -## Reproducing ty ecosystem changes - -If asked to reproduce changes in the ty ecosystem, use this script to clone the project to some -directory and install its dependencies into `.venv`: - -```sh -uv run scripts/setup_primer_project.py -``` - -## Pull Requests - -When working on ty, PR titles should start with `[ty]` and be tagged with the `ty` GitHub label. - -## Development Guidelines - -- All changes must be tested. If you're not testing your changes, you're not done. -- Look to see if your tests could go in an existing file before adding a new file for your tests. -- Get your tests to pass. If you didn't run the tests, your code does not work. -- Follow existing code style. Check neighboring files for patterns. -- Rust imports should always go at the top of the file, never locally in functions. -- Always run `uvx prek run -a` at the end of a task, after every rebase, after addressing any review comment, and before pushing any code. -- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first. -- Try hard to avoid patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls. -- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability. At the end of a task, always check your work to see if you missed opportunities to use `let` chains. -- If you *have* to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. But if a lint is complaining about unused/dead code, it's usually best to just delete the unused code. -- Use comments purposefully. Don't use comments to narrate code, but do use them to explain invariants and why something unusual was done a particular way. -- When adding new ty checks, it's important to make error messages concise. Think about how an error message would look on a narrow terminal screen. Sometimes more detail can be provided in subdiagnostics or secondary annotations, but it's also important to make sure that the diagnostic is understandable if the user has passed `--output-format=concise`. -- **Salsa incrementality (ty):** Any method that accesses `.node()` must be `#[salsa::tracked]`, or it will break incrementality. Prefer higher-level semantic APIs over raw AST access. -- Run `cargo dev generate-all` after changing configuration options, CLI arguments, lint rules, or environment variable definitions, as these changes require regeneration of schemas, docs, and CLI references. +@AGENTS.md From 53ad26f1e10b749e1ef4680603aa9156dd528dc5 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 8 Mar 2026 13:22:01 +0000 Subject: [PATCH 242/261] [ty] Split out more submodules from `types/infer/builder.rs` (#23801) --- .../src/types/infer/builder.rs | 8075 ++++------------- .../src/types/infer/builder/class.rs | 257 + .../src/types/infer/builder/function.rs | 884 ++ .../src/types/infer/builder/imports.rs | 662 ++ .../src/types/infer/builder/named_tuple.rs | 784 ++ .../src/types/infer/builder/subscript.rs | 783 +- .../src/types/infer/builder/typevar.rs | 1064 +++ 7 files changed, 6320 insertions(+), 6189 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/infer/builder/class.rs create mode 100644 crates/ty_python_semantic/src/types/infer/builder/function.rs create mode 100644 crates/ty_python_semantic/src/types/infer/builder/imports.rs create mode 100644 crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs create mode 100644 crates/ty_python_semantic/src/types/infer/builder/typevar.rs diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 567cdef0eb47b..29647007328c8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1,27 +1,21 @@ use std::borrow::Cow; use itertools::{Either, Itertools}; -use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; +use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity}; use ruff_db::files::File; -use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_db::parsed::ParsedModuleRef; use ruff_db::source::source_text; use ruff_python_ast::name::Name; -use ruff_python_ast::visitor::{Visitor, walk_expr}; use ruff_python_ast::{ self as ast, AnyNodeRef, ArgOrKeyword, ArgumentsSourceOrder, ExprContext, HasNodeIndex, NodeIndex, PythonVersion, }; use ruff_python_stdlib::builtins::version_builtin_was_added; -use ruff_python_stdlib::identifiers::is_identifier; -use ruff_python_stdlib::keyword::is_keyword; use ruff_python_stdlib::typing::as_pep_585_generic; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; -use ty_module_resolver::{ - KnownModule, ModuleName, ModuleNameResolutionError, ModuleResolveMode, file_to_module, - resolve_module, search_paths, -}; +use ty_module_resolver::{KnownModule, ModuleName, resolve_module}; use super::deferred; use super::{ @@ -56,57 +50,44 @@ use crate::semantic_index::symbol::{ScopedSymbolId, Symbol}; use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, }; -use crate::types::BindingContext; use crate::types::CallableTypes; use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::callable::CallableTypeKind; use crate::types::class::{ ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral, - DynamicMetaclassConflict, DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, MethodDecorator, - NamedTupleField, NamedTupleSpec, + DynamicMetaclassConflict, MethodDecorator, }; use crate::types::constraints::ConstraintSetBuilder; -use crate::types::context::{InNoTypeCheck, InferContext}; +use crate::types::context::InferContext; use crate::types::diagnostic::{ self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CYCLIC_CLASS_DEFINITION, - CYCLIC_TYPE_ALIAS_DEFINITION, DUPLICATE_BASE, FINAL_ON_NON_METHOD, INCONSISTENT_MRO, - INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, - INVALID_BASE, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_KEY, - INVALID_LEGACY_TYPE_VARIABLE, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, + CYCLIC_TYPE_ALIAS_DEFINITION, DUPLICATE_BASE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, + INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, + INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_LEGACY_TYPE_VARIABLE, + INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, - INVALID_TYPE_VARIABLE_DEFAULT, IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, - PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, - POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, - TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, - UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, - UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, USELESS_OVERLOAD_BODY, - hint_if_stdlib_attribute_exists_on_other_versions, - hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, - report_bad_dunder_set_call, report_call_to_abstract_method, - report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict, - report_conflicting_metaclass_from_bases, report_implicit_return_type, - report_instance_layout_conflict, report_invalid_assignment, - report_invalid_attribute_assignment, report_invalid_class_match_pattern, - report_invalid_exception_caught, report_invalid_exception_cause, - report_invalid_exception_raised, report_invalid_exception_tuple_caught, - report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, - report_invalid_return_type, report_invalid_type_checking_constant, + IncompatibleBases, NO_MATCHING_OVERLOAD, POSSIBLY_MISSING_ATTRIBUTE, + POSSIBLY_MISSING_IMPLICIT_CALL, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, + UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, + UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, hint_if_stdlib_attribute_exists_on_other_versions, + report_attempted_protocol_instantiation, report_bad_dunder_set_call, + report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict, + report_conflicting_metaclass_from_bases, report_instance_layout_conflict, + report_invalid_assignment, report_invalid_attribute_assignment, + report_invalid_class_match_pattern, report_invalid_exception_caught, + report_invalid_exception_cause, report_invalid_exception_raised, + report_invalid_exception_tuple_caught, report_invalid_key_on_typed_dict, + report_invalid_type_checking_constant, report_match_pattern_against_non_runtime_checkable_protocol, - report_match_pattern_against_typed_dict, report_not_subscriptable, - report_possibly_missing_attribute, report_possibly_unresolved_reference, - report_shadowed_type_variable, report_unsupported_augmented_assignment, + report_match_pattern_against_typed_dict, report_possibly_missing_attribute, + report_possibly_unresolved_reference, report_unsupported_augmented_assignment, report_unsupported_comparison, }; use crate::types::enums::{enum_ignored_names, is_enum_class_by_inheritance}; -use crate::types::function::{ - FunctionBodyKind, FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, - OverloadLiteral, function_body_kind, is_implicit_classmethod, -}; -use crate::types::generics::{ - InferableTypeVars, SpecializationBuilder, bind_typevar, enclosing_generic_contexts, typing_self, -}; +use crate::types::function::{FunctionType, KnownFunction}; +use crate::types::generics::{InferableTypeVars, SpecializationBuilder, bind_typevar}; +use crate::types::infer::builder::named_tuple::NamedTupleKind; use crate::types::infer::builder::paramspec_validation::validate_paramspec_components; use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function}; use crate::types::mro::DynamicMroErrorKind; @@ -115,24 +96,17 @@ use crate::types::set_theoretic::RecursivelyDefined; use crate::types::subclass_of::SubclassOfInner; use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType}; use crate::types::type_alias::{ManualPEP695TypeAliasType, PEP695TypeAliasType}; -use crate::types::typed_dict::{ - TypedDictAssignmentKind, TypedDictKeyAssignment, validate_typed_dict_constructor, - validate_typed_dict_dict_literal, -}; -use crate::types::typevar::{ - BoundTypeVarIdentity, TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, - TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, -}; -use crate::types::visitor::find_over_type; +use crate::types::typed_dict::{validate_typed_dict_constructor, validate_typed_dict_dict_literal}; +use crate::types::typevar::{BoundTypeVarIdentity, TypeVarConstraints, TypeVarIdentity}; use crate::types::{ - CallDunderError, CallableBinding, CallableType, ClassType, DataclassParams, DynamicType, - EvaluationMode, InferenceFlags, InternedConstraintSet, InternedType, IntersectionBuilder, - IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard, - LiteralValueTypeKind, MemberLookupPolicy, ParamSpecAttrKind, Parameter, ParameterForm, - Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType, Truthiness, Type, - TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, binding_type, - definition_expression_type, infer_complete_scope_types, infer_scope_types, todo_type, + CallDunderError, CallableBinding, CallableType, ClassType, DynamicType, EvaluationMode, + InferenceFlags, InternedConstraintSet, InternedType, IntersectionBuilder, IntersectionType, + KnownClass, KnownInstanceType, KnownUnion, LiteralValueTypeKind, MemberLookupPolicy, + ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, + SubclassOfType, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, + TypeQualifiers, TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, TypedDictType, + UnionBuilder, UnionType, binding_type, definition_expression_type, infer_complete_scope_types, + infer_scope_types, todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::UnpackPosition; @@ -140,9 +114,14 @@ use crate::{AnalysisSettings, Db, FxIndexSet, Program}; mod annotation_expression; mod binary_expressions; +mod class; +mod function; +mod imports; +mod named_tuple; mod paramspec_validation; mod subscript; mod type_expression; +mod typevar; use super::comparisons::{self, BinaryComparisonVisitor}; @@ -1235,60 +1214,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_body(&module.body); } - fn infer_class_type_params(&mut self, class: &ast::StmtClassDef) { - let type_params = class - .type_params - .as_deref() - .expect("class type params scope without type params"); - - let binding_context = self.index.expect_single_definition(class); - let previous_typevar_binding_context = - self.typevar_binding_context.replace(binding_context); - - self.infer_type_parameters(type_params); - - if let Some(arguments) = class.arguments.as_deref() { - let in_stub = self.in_stub(); - let previous_deferred_state = - std::mem::replace(&mut self.deferred_state, in_stub.into()); - let mut call_arguments = - CallArguments::from_arguments(arguments, |argument, splatted_value| { - let ty = self.infer_expression(splatted_value, TypeContext::default()); - if let Some(argument) = argument { - self.store_expression_type(argument, ty); - } - ty - }); - let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; - self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); - self.deferred_state = previous_deferred_state; - } - - self.typevar_binding_context = previous_typevar_binding_context; - } - - fn infer_class_body(&mut self, class: &ast::StmtClassDef) { - self.infer_body(&class.body); - } - - fn infer_function_type_params(&mut self, function: &ast::StmtFunctionDef) { - let type_params = function - .type_params - .as_deref() - .expect("function type params scope without type params"); - - let binding_context = self.index.expect_single_definition(function); - let previous_typevar_binding_context = - self.typevar_binding_context.replace(binding_context); - self.infer_return_type_annotation( - function.returns.as_deref(), - self.defer_annotations().into(), - ); - self.infer_type_parameters(type_params); - self.infer_parameters(&function.parameters); - self.typevar_binding_context = previous_typevar_binding_context; - } - fn infer_type_alias_type_params(&mut self, type_alias: &ast::StmtTypeAlias) { let type_params = type_alias .type_params @@ -1409,129 +1334,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) } - fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) { - // Parameters are odd: they are Definitions in the function body scope, but have no - // constituent nodes that are part of the function body. In order to get diagnostics - // merged/emitted for them, we need to explicitly infer their definitions here. - for parameter in &function.parameters { - self.infer_definition(parameter); - } - - validate_paramspec_components(&self.context, &function.parameters, |expr| { - self.file_expression_type(expr) - }); - - self.infer_body(&function.body); - - if let Some(returns) = function.returns.as_deref() { - let has_empty_body = self.return_types_and_ranges.is_empty() - && function_body_kind(self.db(), function, |expr| self.expression_type(expr)) - == FunctionBodyKind::Stub; - - let mut enclosing_class_context = None; - - if has_empty_body { - if self.in_stub() { - return; - } - if self.in_function_overload_or_abstractmethod() { - return; - } - if self.scope().scope(self.db()).in_type_checking_block() { - return; - } - if let Some(class) = self.class_context_of_current_method() { - enclosing_class_context = Some(class); - if class.is_protocol(self.db()) { - return; - } - } - } - - let enclosing_function = - nearest_enclosing_function(self.db(), self.index, self.scope()) - .expect("should be in a function body scope"); - let declared_ty = enclosing_function - .last_definition_raw_signature(self.db()) - .return_ty; - let expected_ty = match declared_ty { - Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(self.db()), - ty => ty, - }; - - let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); - if scope_id.is_generator_function(self.index) { - // TODO: `AsyncGeneratorType` and `GeneratorType` are both generic classes. - // - // If type arguments are supplied to `(Async)Iterable`, `(Async)Iterator`, - // `(Async)Generator` or `(Async)GeneratorType` in the return annotation, - // we should iterate over the `yield` expressions and `return` statements - // in the function to check that they are consistent with the type arguments - // provided. Once we do this, the `.to_instance_unknown` call below should - // be replaced with `.to_specialized_instance`. - let inferred_return = if function.is_async { - KnownClass::AsyncGeneratorType - } else { - KnownClass::GeneratorType - }; - - if !inferred_return - .to_instance_unknown(self.db()) - .is_assignable_to(self.db(), expected_ty) - { - report_invalid_generator_function_return_type( - &self.context, - returns.range(), - inferred_return, - declared_ty, - ); - } - return; - } - - for invalid in self - .return_types_and_ranges - .iter() - .copied() - .filter_map(|ty_range| match ty_range.ty { - // We skip `is_assignable_to` checks for `NotImplemented`, - // so we remove it beforehand. - Type::Union(union) => Some(TypeAndRange { - ty: union.filter(self.db(), |ty| !ty.is_notimplemented(self.db())), - range: ty_range.range, - }), - ty if ty.is_notimplemented(self.db()) => None, - _ => Some(ty_range), - }) - .filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), expected_ty)) - { - report_invalid_return_type( - &self.context, - invalid.range, - returns.range(), - declared_ty, - invalid.ty, - ); - } - if self - .index - .use_def_map(scope_id) - .can_implicitly_return_none(self.db()) - && !Type::none(self.db()).is_assignable_to(self.db(), expected_ty) - { - let no_return = self.return_types_and_ranges.is_empty(); - report_implicit_return_type( - &self.context, - returns.range(), - declared_ty, - has_empty_body, - enclosing_class_context, - no_return, - ); - } - } - } - fn infer_body(&mut self, suite: &[ast::Stmt]) { for statement in suite { self.infer_statement(statement); @@ -1597,961 +1399,382 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.extend_definition(result); } - fn infer_function_definition_statement(&mut self, function: &ast::StmtFunctionDef) { - self.infer_definition(function); - } - - fn infer_function_definition( + fn infer_type_alias_definition( &mut self, - function: &ast::StmtFunctionDef, + type_alias: &ast::StmtTypeAlias, definition: Definition<'db>, ) { - let ast::StmtFunctionDef { - range: _, - node_index: _, - is_async: _, - name, - type_params, - parameters, - returns: _, - body: _, - decorator_list, - } = function; - - let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len()); - let mut function_decorators = FunctionDecorators::empty(); - let mut deprecated = None; - let mut dataclass_transformer_params = None; - let mut final_decorator = None; - - for decorator in decorator_list { - let decorator_type = self.infer_decorator(decorator); - let decorator_function_decorator = - FunctionDecorators::from_decorator_type(self.db(), decorator_type); - function_decorators |= decorator_function_decorator; - - match decorator_type { - Type::FunctionLiteral(function) => match function.known(self.db()) { - Some(KnownFunction::NoTypeCheck) => { - // If the function is decorated with the `no_type_check` decorator, - // we need to suppress any errors that come after the decorators. - self.context.set_in_no_type_check(InNoTypeCheck::Yes); - continue; - } - Some(KnownFunction::Final) => { - final_decorator = Some(decorator); - continue; - } - _ => {} - }, - Type::KnownInstance(KnownInstanceType::Deprecated(deprecated_inst)) => { - deprecated = Some(deprecated_inst); - } - Type::DataclassTransformer(params) => { - dataclass_transformer_params = Some(params); - } - _ => {} - } - if !decorator_function_decorator.is_empty() { - continue; - } - - decorator_types_and_nodes.push((decorator_type, decorator)); - } - - // Check for `@final` applied to non-method functions. - // `@final` is only meaningful on methods and classes. - if let Some(final_decorator) = final_decorator - && !self - .index - .scope(self.scope().file_scope_id(self.db())) - .kind() - .is_class() - && let Some(builder) = self - .context - .report_lint(&FINAL_ON_NON_METHOD, final_decorator) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "`@final` cannot be applied to non-method function `{name}`", - )); - diagnostic.info("`@final` is only meaningful on methods and classes"); - } - - let has_defaults = parameters - .iter_non_variadic_params() - .any(|param| param.default.is_some()); - - // If there are type params, parameters and returns are evaluated in that scope. Otherwise, - // we always defer the inference of the parameters and returns. That ensures that we do not - // add any spurious salsa cycles when applying decorators below. (Applying a decorator - // requires getting the signature of this function definition, which in turn requires - // (lazily) inferring the parameter and return types.) If defaults exist, we also defer so - // they can be inferred once with type context in the enclosing scope. - if type_params.is_none() || has_defaults { - self.deferred.insert(definition, self.multi_inference_state); - } - - let known_function = - KnownFunction::try_from_definition_and_name(self.db(), definition, name); - - // `type_check_only` is itself not available at runtime - if known_function == Some(KnownFunction::TypeCheckOnly) { - function_decorators |= FunctionDecorators::TYPE_CHECK_ONLY; - } + self.infer_expression(&type_alias.name, TypeContext::default()); - let body_scope = self + let rhs_scope = self .index - .node_scope(NodeWithScopeRef::Function(function)) + .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) .to_scope_id(self.db(), self.file()); - let overload_literal = OverloadLiteral::new( - self.db(), - &name.id, - known_function, - body_scope, - function_decorators, - deprecated, - dataclass_transformer_params, - ); - let function_literal = FunctionLiteral::new(self.db(), overload_literal); - - let mut inferred_ty = - Type::FunctionLiteral(FunctionType::new(self.db(), function_literal, None, None)); - self.undecorated_type = Some(inferred_ty); - - // Check that the function's own type parameters don't shadow - // type variables from enclosing scopes (by name). - if let Some(type_params) = &function.type_params { - let current_scope = self.scope().file_scope_id(self.db()); - for type_param in type_params.iter() { - let param_name = type_param.name(); - for enclosing in enclosing_generic_contexts(self.db(), self.index, current_scope) { - if let Some(other_typevar) = - enclosing.binds_named_typevar(self.db(), ¶m_name.id) - { - report_shadowed_type_variable( - &self.context, - ¶m_name.id, - "function", - &function.name.id, - function.name.range(), - other_typevar, - ); - } - } - } - } - - for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { - inferred_ty = self.apply_decorator(*decorator_ty, inferred_ty, decorator_node); - } + let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType( + TypeAliasType::PEP695(PEP695TypeAliasType::new( + self.db(), + &type_alias.name.as_name_expr().unwrap().id, + rhs_scope, + None, + )), + )); self.add_declaration_with_binding( - function.into(), + type_alias.into(), definition, - &DeclaredAndInferredType::are_the_same_type(inferred_ty), + &DeclaredAndInferredType::are_the_same_type(type_alias_ty), ); - - if function_decorators.contains(FunctionDecorators::OVERLOAD) { - for stmt in &function.body { - match stmt { - ast::Stmt::Pass(_) => continue, - ast::Stmt::Expr(ast::StmtExpr { value, .. }) => { - if matches!( - &**value, - ast::Expr::StringLiteral(_) | ast::Expr::EllipsisLiteral(_) - ) { - continue; - } - } - _ => {} - } - let Some(builder) = self.context.report_lint(&USELESS_OVERLOAD_BODY, stmt) else { - continue; - }; - let mut diagnostic = builder.into_diagnostic(format_args!( - "Useless body for `@overload`-decorated function `{}`", - &function.name - )); - diagnostic.set_primary_message("This statement will never be executed"); - diagnostic.info( - "`@overload`-decorated functions are solely for type checkers \ - and must be overwritten at runtime by a non-`@overload`-decorated implementation", - ); - diagnostic.help("Consider replacing this function body with `...` or `pass`"); - break; - } - } - } - - fn infer_return_type_annotation( - &mut self, - returns: Option<&ast::Expr>, - deferred_expression_state: DeferredExpressionState, - ) { - let Some(returns) = returns else { - return; - }; - let annotated = self.infer_annotation_expression(returns, deferred_expression_state); - - if annotated.qualifiers.is_empty() { - return; - } - for qualifier in [ - TypeQualifiers::FINAL, - TypeQualifiers::CLASS_VAR, - TypeQualifiers::INIT_VAR, - ] { - if annotated.qualifiers.contains(qualifier) - && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, returns) - { - builder.into_diagnostic(format!( - "`{name}` is not allowed in function return type annotations", - name = qualifier.name() - )); - } - } } - fn infer_parameters(&mut self, parameters: &ast::Parameters) { - let ast::Parameters { + fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) { + let ast::StmtIf { range: _, node_index: _, - posonlyargs: _, - args: _, - vararg, - kwonlyargs: _, - kwarg, - } = parameters; - - for param_with_default in parameters.iter_non_variadic_params() { - self.infer_parameter_with_default(param_with_default); - } - if let Some(vararg) = vararg { - self.inferring_vararg_annotation = true; - self.infer_parameter(vararg); - self.inferring_vararg_annotation = false; - } - if let Some(kwarg) = kwarg { - self.infer_parameter(kwarg); - } - } + test, + body, + elif_else_clauses, + } = if_statement; - fn infer_parameter_with_default(&mut self, parameter_with_default: &ast::ParameterWithDefault) { - let ast::ParameterWithDefault { - range: _, - node_index: _, - parameter, - default: _, - } = parameter_with_default; + let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - let annotated = self.infer_optional_annotation_expression( - parameter.annotation.as_deref(), - self.defer_annotations().into(), - ); + if let Err(err) = test_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, &**test); + } - let Some(annotated) = annotated else { - return; - }; + self.infer_body(body); - let qualifiers = annotated.qualifiers; + for clause in elif_else_clauses { + let ast::ElifElseClause { + range: _, + node_index: _, + test, + body, + } = clause; - if qualifiers.is_empty() { - return; - } + if let Some(test) = &test { + let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - for qualifier in [ - TypeQualifiers::FINAL, - TypeQualifiers::CLASS_VAR, - TypeQualifiers::INIT_VAR, - TypeQualifiers::REQUIRED, - TypeQualifiers::NOT_REQUIRED, - TypeQualifiers::READ_ONLY, - ] { - if qualifiers.contains(qualifier) - && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, parameter) - { - builder.into_diagnostic(format!( - "`{name}` is not allowed in function parameter annotations", - name = qualifier.name() - )); + if let Err(err) = test_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, test); + } } + + self.infer_body(body); } } - fn infer_parameter(&mut self, parameter: &ast::Parameter) { - let ast::Parameter { + fn infer_try_statement(&mut self, try_statement: &ast::StmtTry) { + let ast::StmtTry { range: _, node_index: _, - name: _, - annotation, - } = parameter; + body, + handlers, + orelse, + finalbody, + is_star: _, + } = try_statement; - self.infer_optional_annotation_expression( - annotation.as_deref(), - self.defer_annotations().into(), - ); + self.infer_body(body); + + for handler in handlers { + let ast::ExceptHandler::ExceptHandler(handler) = handler; + let ast::ExceptHandlerExceptHandler { + type_: handled_exceptions, + name: symbol_name, + body, + range: _, + node_index: _, + } = handler; + + // If `symbol_name` is `Some()` and `handled_exceptions` is `None`, + // it's invalid syntax (something like `except as e:`). + // However, it's obvious that the user *wanted* `e` to be bound here, + // so we'll have created a definition in the semantic-index stage anyway. + if symbol_name.is_some() { + self.infer_definition(handler); + } else { + self.infer_exception(handled_exceptions.as_deref(), try_statement.is_star); + } + + self.infer_body(body); + } + + self.infer_body(orelse); + self.infer_body(finalbody); } - /// Set initial declared type (if annotated) and inferred type for a function-parameter symbol, - /// in the function body scope. - /// - /// The declared type is the annotated type, if any, or `Unknown`. - /// - /// The inferred type is the annotated type, if any. If there is no annotation, it is the union - /// of `Unknown` and the type of the default value, if any. - /// - /// Parameter definitions are odd in that they define a symbol in the function-body scope, so - /// the Definition belongs to the function body scope, but the expressions (annotation and - /// default value) both belong to outer scopes. (The default value always belongs to the outer - /// scope in which the function is defined, the annotation belongs either to the outer scope, - /// or maybe to an intervening type-params scope, if it's a generic function.) So we don't use - /// `self.infer_expression` or store any expression types here, we just query for the types of - /// the expressions from their respective scopes. - /// - /// It is safe (non-cycle-causing) to query the annotation type via `file_expression_type` - /// here, because an outer scope can't depend on a definition from an inner scope, so we - /// shouldn't be in-process of inferring the outer scope here. - fn infer_parameter_definition( - &mut self, - parameter_with_default: &'ast ast::ParameterWithDefault, - definition: Definition<'db>, - ) { - let ast::ParameterWithDefault { - parameter, - default, + fn infer_with_statement(&mut self, with_statement: &ast::StmtWith) { + let ast::StmtWith { range: _, node_index: _, - } = parameter_with_default; - let default_expr = default.as_ref(); - if let Some(annotation) = parameter.annotation.as_ref() { - let declared_ty = self.file_expression_type(annotation); - - // P.args and P.kwargs are only valid as annotations on *args and **kwargs, - // not on regular parameters. - if let Type::TypeVar(typevar) = declared_ty - && typevar.is_paramspec(self.db()) - && let Some(attr) = typevar.paramspec_attr(self.db()) - { - let name = typevar.name(self.db()); - let (attr_name, variadic) = match attr { - ParamSpecAttrKind::Args => ("args", "*args"), - ParamSpecAttrKind::Kwargs => ("kwargs", "**kwargs"), - }; - if let Some(builder) = self - .context - .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) - { - builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}`", - )); - } - } - - if let Some(default_expr) = default_expr { - let default_expr = default_expr.as_ref(); - let default_ty = self.file_expression_type(default_expr); - - // Avoid duplicate diagnostics: invalid TypedDict literals already emit specific errors. - let suppress_invalid_default = diagnostic::is_invalid_typed_dict_literal( - self.db(), - declared_ty, - default_expr.into(), - ); - if !default_ty.is_assignable_to(self.db(), declared_ty) - && !suppress_invalid_default - && !((self.in_stub() - || self.in_function_overload_or_abstractmethod() - || self.scope().scope(self.db()).in_type_checking_block() - || self - .class_context_of_current_method() - .is_some_and(|class| class.is_protocol(self.db()))) - && default - .as_ref() - .is_some_and(|d| d.is_ellipsis_literal_expr())) - { - if let Some(builder) = self - .context - .report_lint(&INVALID_PARAMETER_DEFAULT, parameter_with_default) - { - builder.into_diagnostic(format_args!( - "Default value of type `{}` is not assignable \ - to annotated parameter type `{}`", - default_ty.display(self.db()), - declared_ty.display(self.db()) - )); - } - } - } - - self.add_declaration_with_binding( - parameter.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(declared_ty), - ); - } else { - let ty = if let Some(default_expr) = default_expr { - let default_ty = self.file_expression_type(default_expr); - UnionType::from_two_elements(self.db(), Type::unknown(), default_ty) - } else if let Some(ty) = self.special_first_method_parameter_type(parameter) { - ty + is_async, + items, + body, + } = with_statement; + for item in items { + let target = item.optional_vars.as_deref(); + if let Some(target) = target { + self.infer_target(target, &item.context_expr, &|builder, tcx| { + // TODO: `infer_with_statement_definition` reports a diagnostic if `ctx_manager_ty` isn't a context manager + // but only if the target is a name. We should report a diagnostic here if the target isn't a name: + // `with not_context_manager as a.x: ... + builder + .infer_standalone_expression(&item.context_expr, tcx) + .enter(builder.db()) + }); } else { - Type::unknown() - }; - - self.add_binding(parameter.into(), definition) - .insert(self, ty); + // Call into the context expression inference to validate that it evaluates + // to a valid context manager. + let context_expression_ty = + self.infer_expression(&item.context_expr, TypeContext::default()); + self.infer_context_expression(&item.context_expr, context_expression_ty, *is_async); + self.infer_optional_expression(target, TypeContext::default()); + } } + + self.infer_body(body); } - /// Set initial declared/inferred types for a `*args` variadic positional parameter. - /// - /// The annotated type is implicitly wrapped in a homogeneous tuple. - /// - /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. - /// - /// [`infer_parameter_definition`]: Self::infer_parameter_definition - fn infer_variadic_positional_parameter_definition( + fn infer_with_item_definition( &mut self, - parameter: &'ast ast::Parameter, + with_item: &WithItemDefinitionKind<'db>, definition: Definition<'db>, ) { - if let Some(annotation) = parameter.annotation() { - let ty = if annotation.is_starred_expr() { - todo_type!("PEP 646") - } else { - let annotated_type = self.file_expression_type(annotation); - if let Type::TypeVar(typevar) = annotated_type - && typevar.is_paramspec(self.db()) - { - match typevar.paramspec_attr(self.db()) { - // `*args: P.args` - Some(ParamSpecAttrKind::Args) => annotated_type, - - // `*args: P.kwargs` - Some(ParamSpecAttrKind::Kwargs) => { - // TODO: Should this diagnostic be raised as part of - // `ArgumentTypeChecker`? - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, annotation) - { - let name = typevar.name(self.db()); - let mut diag = builder.into_diagnostic(format_args!( - "`{name}.kwargs` is valid only in `**kwargs` annotation", - )); - diag.set_primary_message(format_args!( - "Did you mean `{name}.args`?" - )); - diagnostic::add_type_expression_reference_link(diag); - } - Type::homogeneous_tuple(self.db(), Type::unknown()) - } + let context_expr = with_item.context_expr(self.module()); + let target = with_item.target(self.module()); - // `*args: P` - None => { - // The diagnostic for this case is handled in `in_type_expression`. - Type::homogeneous_tuple(self.db(), Type::unknown()) - } - } - } else { - Type::homogeneous_tuple(self.db(), annotated_type) + let target_ty = match with_item.target_kind() { + TargetKind::Sequence(unpack_position, unpack) => { + let unpacked = infer_unpack_types(self.db(), unpack); + if unpack_position == UnpackPosition::First { + self.context.extend(unpacked.diagnostics()); } - }; + unpacked.expression_type(target) + } + TargetKind::Single => { + let context_expr_ty = + self.infer_standalone_expression(context_expr, TypeContext::default()); + self.infer_context_expression(context_expr, context_expr_ty, with_item.is_async()) + } + }; - self.add_declaration_with_binding( - parameter.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(ty), - ); - } else { - let inferred_ty = Type::homogeneous_tuple(self.db(), Type::unknown()); - self.add_binding(parameter.into(), definition) - .insert(self, inferred_ty); - } + self.store_expression_type(target, target_ty); + self.add_binding(target.into(), definition) + .insert(self, target_ty); } - /// Special case for unannotated `cls` and `self` arguments to class methods and instance methods. - fn special_first_method_parameter_type( + /// Infers the type of a context expression (`with expr`) and returns the target's type + /// + /// Returns [`Type::unknown`] if the context expression doesn't implement the context manager protocol. + /// + /// ## Terminology + /// See [PEP343](https://peps.python.org/pep-0343/#standard-terminology). + fn infer_context_expression( &mut self, - parameter: &ast::Parameter, - ) -> Option> { - let db = self.db(); - let file = self.file(); - - let function_scope_id = self.scope(); - let function_scope = function_scope_id.scope(db); - let function = function_scope.node().as_function()?; - - let parent_file_scope_id = function_scope.parent()?; - let mut parent_scope_id = parent_file_scope_id.to_scope_id(db, file); - - // Skip type parameter scopes, if the method itself is generic. - if parent_scope_id.is_annotation(db) { - let parent_scope = parent_scope_id.scope(db); - parent_scope_id = parent_scope.parent()?.to_scope_id(db, file); - } + context_expression: &ast::Expr, + context_expression_type: Type<'db>, + is_async: bool, + ) -> Type<'db> { + let eval_mode = if is_async { + EvaluationMode::Async + } else { + EvaluationMode::Sync + }; - // Return early if this is not a method inside a class. - let class = parent_scope_id.scope(db).node().as_class()?; + context_expression_type + .try_enter_with_mode(self.db(), eval_mode) + .unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + context_expression_type, + context_expression.into(), + ); + err.fallback_enter_type(self.db()) + }) + } - let method_definition = self.index.expect_single_definition(function); - let DefinitionKind::Function(function_definition) = method_definition.kind(db) else { - return None; - }; + fn infer_exception(&mut self, node: Option<&ast::Expr>, is_star: bool) -> Type<'db> { + // If there is no handled exception, it's invalid syntax; + // a diagnostic will have already been emitted + let node_ty = node.map_or(Type::unknown(), |ty| { + self.infer_expression(ty, TypeContext::default()) + }); + let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db()); - if function_definition - .node(self.module()) - .parameters - .index(parameter.name()) - .is_none_or(|index| index != 0) - { - return None; - } + // If it's an `except*` handler, this won't actually be the type of the bound symbol; + // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. + let symbol_ty = if let Some(tuple_spec) = node_ty.tuple_instance_spec(self.db()) { + let mut builder = UnionBuilder::new(self.db()); + let mut invalid_elements = vec![]; - let function_node = function_definition.node(self.module()); - let function_name = &function_node.name; + for (index, element) in tuple_spec.all_elements().iter().enumerate() { + builder = builder.add( + if element.is_assignable_to(self.db(), type_base_exception) { + element.to_instance(self.db()).expect( + "`Type::to_instance()` should always return `Some()` \ + if called on a type assignable to `type[BaseException]`", + ) + } else { + invalid_elements.push((index, element)); + Type::unknown() + }, + ); + } - let mut is_classmethod = is_implicit_classmethod(function_name); - let inference = infer_definition_types(db, method_definition); - for decorator in &function_node.decorator_list { - let decorator_ty = inference.expression_type(&decorator.expression); - if let Some(known_class) = decorator_ty - .as_class_literal() - .and_then(|class| class.known(db)) + if !invalid_elements.is_empty() + && let Some(node) = node { - if known_class == KnownClass::Staticmethod { - return None; - } + if let ast::Expr::Tuple(tuple) = node + && !tuple.iter().any(ast::Expr::is_starred_expr) + && Some(tuple.len()) == tuple_spec.len().into_fixed_length() + { + let invalid_elements = invalid_elements + .iter() + .map(|(index, ty)| (&tuple.elts[*index], **ty)); - is_classmethod |= known_class == KnownClass::Classmethod; + report_invalid_exception_tuple_caught( + &self.context, + tuple, + node_ty, + invalid_elements, + ); + } else { + report_invalid_exception_caught(&self.context, node, node_ty); + } } - } - - let class_definition = self.index.expect_single_definition(class); - let class_literal = infer_definition_types(db, class_definition) - .declaration_type(class_definition) - .inner_type() - .as_class_literal()?; - let typing_self = typing_self(db, self.scope(), Some(method_definition), class_literal); - if is_classmethod || function_name == "__new__" { - typing_self - .map(|typing_self| SubclassOfType::from(db, SubclassOfInner::TypeVar(typing_self))) - } else { - typing_self.map(Type::TypeVar) - } - } + builder.build() + } else if node_ty.is_assignable_to(self.db(), type_base_exception) { + node_ty.to_instance(self.db()).expect( + "`Type::to_instance()` should always return `Some()` \ + if called on a type assignable to `type[BaseException]`", + ) + } else if node_ty.is_assignable_to( + self.db(), + Type::homogeneous_tuple(self.db(), type_base_exception), + ) { + node_ty + .tuple_instance_spec(self.db()) + .and_then(|spec| { + let specialization = spec + .homogeneous_element_type(self.db()) + .to_instance(self.db()); - /// Set initial declared/inferred types for a `**kwargs` keyword-variadic parameter. - /// - /// The annotated type is implicitly wrapped in a string-keyed dictionary. - /// - /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. - /// - /// [`infer_parameter_definition`]: Self::infer_parameter_definition - fn infer_variadic_keyword_parameter_definition( - &mut self, - parameter: &'ast ast::Parameter, - definition: Definition<'db>, - ) { - if let Some(annotation) = parameter.annotation() { - let annotated_type = self.file_expression_type(annotation); - let ty = if let Type::TypeVar(typevar) = annotated_type - && typevar.is_paramspec(self.db()) - { - match typevar.paramspec_attr(self.db()) { - // `**kwargs: P.args` - Some(ParamSpecAttrKind::Args) => { - // TODO: Should this diagnostic be raised as part of `ArgumentTypeChecker`? - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, annotation) - { - let name = typevar.name(self.db()); - let mut diag = builder.into_diagnostic(format_args!( - "`{name}.args` is valid only in `*args` annotation", - )); - diag.set_primary_message(format_args!("Did you mean `{name}.kwargs`?")); - diagnostic::add_type_expression_reference_link(diag); - } - KnownClass::Dict.to_specialized_instance( + debug_assert!(specialization.is_some_and(|specialization_type| { + specialization_type.is_assignable_to( self.db(), - &[KnownClass::Str.to_instance(self.db()), Type::unknown()], + KnownClass::BaseException.to_instance(self.db()), ) - } + })); - // `**kwargs: P.kwargs` - Some(ParamSpecAttrKind::Kwargs) => annotated_type, + specialization + }) + .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db())) + } else if node_ty.is_assignable_to( + self.db(), + UnionType::from_two_elements( + self.db(), + type_base_exception, + Type::homogeneous_tuple(self.db(), type_base_exception), + ), + ) { + KnownClass::BaseException.to_instance(self.db()) + } else { + if let Some(node) = node { + report_invalid_exception_caught(&self.context, node, node_ty); + } + Type::unknown() + }; - // `**kwargs: P` - None => { - // The diagnostic for this case is handled in `in_type_expression`. - KnownClass::Dict.to_specialized_instance( - self.db(), - &[KnownClass::Str.to_instance(self.db()), Type::unknown()], - ) - } - } + if is_star { + let class = if symbol_ty + .is_subtype_of(self.db(), KnownClass::Exception.to_instance(self.db())) + { + KnownClass::ExceptionGroup } else { - KnownClass::Dict.to_specialized_instance( - self.db(), - &[KnownClass::Str.to_instance(self.db()), annotated_type], - ) + KnownClass::BaseExceptionGroup }; - self.add_declaration_with_binding( - parameter.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(ty), - ); + class.to_specialized_instance(self.db(), &[symbol_ty]) } else { - let inferred_ty = KnownClass::Dict.to_specialized_instance( - self.db(), - &[KnownClass::Str.to_instance(self.db()), Type::unknown()], - ); - - self.add_binding(parameter.into(), definition) - .insert(self, inferred_ty); + symbol_ty } } - fn infer_class_definition_statement(&mut self, class: &ast::StmtClassDef) { - self.infer_definition(class); - } - - fn infer_class_definition( + fn infer_except_handler_definition( &mut self, - class_node: &ast::StmtClassDef, + except_handler_definition: &ExceptHandlerDefinitionKind, definition: Definition<'db>, ) { - let ast::StmtClassDef { - range: _, - node_index: _, - name, - type_params, - decorator_list, - arguments: _, - body: _, - } = class_node; - - let mut decorator_types_and_nodes: Vec<(Type<'db>, &ast::Decorator)> = - Vec::with_capacity(decorator_list.len()); - let mut deprecated = None; - let mut type_check_only = false; - let mut dataclass_params = None; - let mut dataclass_transformer_params = None; - let mut total_ordering = false; - for decorator in decorator_list { - let decorator_ty = self.infer_decorator(decorator); - if decorator_ty - .as_function_literal() - .is_some_and(|function| function.is_known(self.db(), KnownFunction::Dataclass)) - { - dataclass_params = Some(DataclassParams::default_params(self.db())); - continue; - } - - if decorator_ty - .as_function_literal() - .is_some_and(|function| function.is_known(self.db(), KnownFunction::TotalOrdering)) - { - total_ordering = true; - continue; - } - - if let Type::DataclassDecorator(params) = decorator_ty { - dataclass_params = Some(params); - continue; - } - - if let Type::KnownInstance(KnownInstanceType::Deprecated(deprecated_inst)) = - decorator_ty - { - deprecated = Some(deprecated_inst); - continue; - } - - if decorator_ty - .as_function_literal() - .is_some_and(|function| function.is_known(self.db(), KnownFunction::TypeCheckOnly)) - { - type_check_only = true; - continue; - } - - // Skip identity decorators to avoid salsa cycles on typeshed. - if decorator_ty.as_function_literal().is_some_and(|function| { - matches!( - function.known(self.db()), - Some( - KnownFunction::Final - | KnownFunction::DisjointBase - | KnownFunction::RuntimeCheckable - ) - ) - }) { - continue; - } - - if let Type::FunctionLiteral(f) = decorator_ty { - // We do not yet detect or flag `@dataclass_transform` applied to more than one - // overload, or an overload and the implementation both. Nevertheless, this is not - // allowed. We do not try to treat the offenders intelligently -- just use the - // params of the last seen usage of `@dataclass_transform` - let transformer_params = f - .iter_overloads_and_implementation(self.db()) - .rev() - .find_map(|overload| overload.dataclass_transformer_params(self.db())); - if let Some(transformer_params) = transformer_params { - dataclass_params = Some(DataclassParams::from_transformer_params( - self.db(), - transformer_params, - )); - continue; - } - } - - if let Type::DataclassTransformer(params) = decorator_ty { - dataclass_transformer_params = Some(params); - continue; - } - - decorator_types_and_nodes.push((decorator_ty, decorator)); - } - - let body_scope = self - .index - .node_scope(NodeWithScopeRef::Class(class_node)) - .to_scope_id(self.db(), self.file()); - - let maybe_known_class = KnownClass::try_from_file_and_name(self.db(), self.file(), name); - - let in_typing_module = || { - matches!( - file_to_module(self.db(), self.file()).and_then(|module| module.known(self.db())), - Some(KnownModule::Typing | KnownModule::TypingExtensions) - ) - }; - - let inferred_ty = match (maybe_known_class, &*name.id) { - (None, "NamedTuple") if in_typing_module() => { - Type::SpecialForm(SpecialFormType::NamedTuple) - } - (None, "Any") if in_typing_module() => Type::SpecialForm(SpecialFormType::Any), - _ => Type::from(StaticClassLiteral::new( - self.db(), - name.id.clone(), - body_scope, - maybe_known_class, - deprecated, - type_check_only, - dataclass_params, - dataclass_transformer_params, - total_ordering, - )), - }; - - // Validate decorator calls (but don't use return types yet). - for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { - if let Err(CallError(_, bindings)) = - decorator_ty.try_call(self.db(), &CallArguments::positional([inferred_ty])) - { - bindings.report_diagnostics(&self.context, (*decorator_node).into()); - } - } - - self.add_declaration_with_binding( - class_node.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(inferred_ty), + let symbol_ty = self.infer_exception( + except_handler_definition.handled_exceptions(self.module()), + except_handler_definition.is_star(), ); - // if there are type parameters, then the keywords and bases are within that scope - // and we don't need to run inference here - if type_params.is_none() { - // In stub files, keyword values may reference names that are defined later in the file. - let in_stub = self.in_stub(); - let previous_deferred_state = - std::mem::replace(&mut self.deferred_state, in_stub.into()); - for keyword in class_node.keywords() { - self.infer_expression(&keyword.value, TypeContext::default()); - } - self.deferred_state = previous_deferred_state; - - // Inference of bases deferred in stubs, or if any are string literals. - if self.in_stub() || class_node.bases().iter().any(contains_string_literal) { - self.deferred.insert(definition, self.multi_inference_state); - } else { - let previous_typevar_binding_context = - self.typevar_binding_context.replace(definition); - for base in class_node.bases() { - self.infer_expression(base, TypeContext::default()); - } - self.typevar_binding_context = previous_typevar_binding_context; - } - } + self.add_binding( + except_handler_definition.node(self.module()).into(), + definition, + ) + .insert(self, symbol_ty); } - fn infer_function_deferred( + /// Infer the type for a loop header definition. + /// + /// The loop header sees all the bindings that originate in the loop and are visible at a + /// loop-back edge (either the end of the loop body or a `continue` statement). See `struct + /// LoopHeader` in the semantic index for more on how all this fits together. + fn infer_loop_header_definition( &mut self, + loop_header_kind: &LoopHeaderDefinitionKind<'db>, definition: Definition<'db>, - function: &ast::StmtFunctionDef, ) { - let mut prev_in_no_type_check = self.context.set_in_no_type_check(InNoTypeCheck::Yes); - for decorator in &function.decorator_list { - let decorator_type = self.infer_decorator(decorator); - if let Type::FunctionLiteral(function) = decorator_type - && let Some(KnownFunction::NoTypeCheck) = function.known(self.db()) - { - // If the function is decorated with the `no_type_check` decorator, - // we need to suppress any errors that come after the decorators. - prev_in_no_type_check = InNoTypeCheck::Yes; - break; - } - } - self.context.set_in_no_type_check(prev_in_no_type_check); - - let has_type_params = function.type_params.is_some(); - let has_defaults = function - .parameters - .iter_non_variadic_params() - .any(|param| param.default.is_some()); - - let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); - - if !has_type_params { - self.infer_return_type_annotation( - function.returns.as_deref(), - self.defer_annotations().into(), - ); - self.infer_parameters(function.parameters.as_ref()); - } - - if has_defaults { - // In stub files, default values may reference names that are defined later in the file. - let in_stub = self.in_stub(); - let previous_deferred_state = - std::mem::replace(&mut self.deferred_state, in_stub.into()); - - // For generic functions, only defaults are inferred here; annotation types come from - // the type-params scope. - if has_type_params { - let type_params_scope = self - .index - .node_scope(NodeWithScopeRef::FunctionTypeParameters(function)) - .to_scope_id(self.db(), self.file()); - let type_params_inference = - infer_scope_types(self.db(), type_params_scope, TypeContext::default()); - - for param_with_default in function.parameters.iter_non_variadic_params() { - let Some(default) = param_with_default.default.as_deref() else { - continue; - }; - let tcx = param_with_default - .parameter - .annotation - .as_deref() - .map(|annotation| { - TypeContext::new(Some( - type_params_inference.expression_type(annotation), - )) - }) - .unwrap_or_else(TypeContext::default); - self.infer_expression(default, tcx); - } - } else { - for param_with_default in function.parameters.iter_non_variadic_params() { - let Some(default) = param_with_default.default.as_deref() else { - continue; - }; - let tcx = param_with_default - .parameter - .annotation - .as_deref() - .map(|annotation| TypeContext::new(Some(self.expression_type(annotation)))) - .unwrap_or_else(TypeContext::default); - self.infer_expression(default, tcx); - } - } + let db = self.db(); + let place = loop_header_kind.place(); + let use_def = self + .index + .use_def_map(self.scope().file_scope_id(self.db())); + let loop_header = loop_header_reachability(db, definition); - self.deferred_state = previous_deferred_state; - } + let mut union = UnionBuilder::new(db).recursively_defined(RecursivelyDefined::Yes); - self.typevar_binding_context = previous_typevar_binding_context; - } + for reachable_binding in &loop_header.reachable_bindings { + let binding_ty = binding_type(db, reachable_binding.definition); + let narrowed_ty = use_def + .narrowing_evaluator(reachable_binding.narrowing_constraint) + .narrow(db, binding_ty, place); - fn infer_class_deferred(&mut self, definition: Definition<'db>, class: &ast::StmtClassDef) { - let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); - for base in class.bases() { - if self.in_stub() { - self.infer_expression_with_state( - base, - TypeContext::default(), - DeferredExpressionState::Deferred, - ); - } else { - self.infer_expression(base, TypeContext::default()); - } + union.add_in_place(narrowed_ty); } - self.typevar_binding_context = previous_typevar_binding_context; - } - - fn infer_type_alias_definition( - &mut self, - type_alias: &ast::StmtTypeAlias, - definition: Definition<'db>, - ) { - self.infer_expression(&type_alias.name, TypeContext::default()); - - let rhs_scope = self - .index - .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) - .to_scope_id(self.db(), self.file()); - - let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::PEP695(PEP695TypeAliasType::new( - self.db(), - &type_alias.name.as_name_expr().unwrap().id, - rhs_scope, - None, - )), - )); - self.add_declaration_with_binding( - type_alias.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(type_alias_ty), - ); + self.bindings + .insert(definition, union.build(), self.multi_inference_state); } - fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) { - let ast::StmtIf { + fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { + let ast::StmtMatch { range: _, node_index: _, - test, - body, - elif_else_clauses, - } = if_statement; - - let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - - if let Err(err) = test_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, &**test); - } + subject, + cases, + } = match_statement; - self.infer_body(body); + self.infer_standalone_expression(subject, TypeContext::default()); - for clause in elif_else_clauses { - let ast::ElifElseClause { + for case in cases { + let ast::MatchCase { range: _, node_index: _, - test, body, - } = clause; + pattern, + guard, + } = case; + self.infer_match_pattern(pattern); - if let Some(test) = &test { - let test_ty = self.infer_standalone_expression(test, TypeContext::default()); + if let Some(guard) = guard.as_deref() { + let guard_ty = self.infer_standalone_expression(guard, TypeContext::default()); - if let Err(err) = test_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, test); + if let Err(err) = guard_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, guard); } } @@ -2559,3497 +1782,1146 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - fn infer_try_statement(&mut self, try_statement: &ast::StmtTry) { - let ast::StmtTry { - range: _, - node_index: _, - body, - handlers, - orelse, - finalbody, - is_star: _, - } = try_statement; - - self.infer_body(body); - - for handler in handlers { - let ast::ExceptHandler::ExceptHandler(handler) = handler; - let ast::ExceptHandlerExceptHandler { - type_: handled_exceptions, - name: symbol_name, - body, - range: _, - node_index: _, - } = handler; + fn infer_match_pattern_definition( + &mut self, + pattern: &'ast ast::Pattern, + _index: u32, + definition: Definition<'db>, + ) { + // TODO(dhruvmanila): The correct way to infer types here is to perform structural matching + // against the subject expression type (which we can query via `infer_expression_types`) + // and extract the type at the `index` position if the pattern matches. This will be + // similar to the logic in `self.infer_assignment_definition`. + self.add_binding(pattern.into(), definition) + .insert(self, todo_type!("`match` pattern definition types")); + } - // If `symbol_name` is `Some()` and `handled_exceptions` is `None`, - // it's invalid syntax (something like `except as e:`). - // However, it's obvious that the user *wanted* `e` to be bound here, - // so we'll have created a definition in the semantic-index stage anyway. - if symbol_name.is_some() { - self.infer_definition(handler); - } else { - self.infer_exception(handled_exceptions.as_deref(), try_statement.is_star); + fn validate_class_pattern(&mut self, pattern: &ast::PatternMatchClass, cls_ty: Type<'db>) { + if let Type::ClassLiteral(class) = cls_ty { + if class.is_typed_dict(self.db()) { + report_match_pattern_against_typed_dict(&self.context, &*pattern.cls, class); + } else if let Some(protocol_class) = class.into_protocol_class(self.db()) + && !protocol_class.is_runtime_checkable(self.db()) + { + report_match_pattern_against_non_runtime_checkable_protocol( + &self.context, + &*pattern.cls, + protocol_class, + ); } - - self.infer_body(body); + } else if !cls_ty.is_assignable_to(self.db(), KnownClass::Type.to_instance(self.db())) { + report_invalid_class_match_pattern(&self.context, &*pattern.cls, cls_ty); } - - self.infer_body(orelse); - self.infer_body(finalbody); } - fn infer_with_statement(&mut self, with_statement: &ast::StmtWith) { - let ast::StmtWith { - range: _, - node_index: _, - is_async, - items, - body, - } = with_statement; - for item in items { - let target = item.optional_vars.as_deref(); - if let Some(target) = target { - self.infer_target(target, &item.context_expr, &|builder, tcx| { - // TODO: `infer_with_statement_definition` reports a diagnostic if `ctx_manager_ty` isn't a context manager - // but only if the target is a name. We should report a diagnostic here if the target isn't a name: - // `with not_context_manager as a.x: ... - builder - .infer_standalone_expression(&item.context_expr, tcx) - .enter(builder.db()) - }); - } else { - // Call into the context expression inference to validate that it evaluates - // to a valid context manager. - let context_expression_ty = - self.infer_expression(&item.context_expr, TypeContext::default()); - self.infer_context_expression(&item.context_expr, context_expression_ty, *is_async); - self.infer_optional_expression(target, TypeContext::default()); + fn infer_match_pattern(&mut self, pattern: &ast::Pattern) { + // We need to create a standalone expression for each arm of a match statement, since they + // can introduce constraints on the match subject. (Or more accurately, for the match arm's + // pattern, since its the pattern that introduces any constraints, not the body.) Ideally, + // that standalone expression would wrap the match arm's pattern as a whole. But a + // standalone expression can currently only wrap an ast::Expr, which patterns are not. So, + // we need to choose an Expr that can “stand in” for the pattern, which we can wrap in a + // standalone expression. + // + // That said, when inferring the type of a standalone expression, we don't have access to + // its parent or sibling nodes. That means, for instance, that in a class pattern, where + // we are currently using the class name as the standalone expression, we do not have + // access to the class pattern's arguments in the standalone expression inference scope. + // At the moment, we aren't trying to do anything with those arguments when creating a + // narrowing constraint for the pattern. But in the future, if we do, we will have to + // either wrap those arguments in their own standalone expressions, or update Expression to + // be able to wrap other AST node types besides just ast::Expr. + // + // This function is only called for the top-level pattern of a match arm, and is + // responsible for inferring the standalone expression for each supported pattern type. It + // then hands off to `infer_nested_match_pattern` for any subexpressions and subpatterns, + // where we do NOT have any additional standalone expressions to infer through. + // + // TODO(dhruvmanila): Add a Salsa query for inferring pattern types and matching against + // the subject expression: https://github.com/astral-sh/ruff/pull/13147#discussion_r1739424510 + match pattern { + ast::Pattern::MatchValue(match_value) => { + self.infer_standalone_expression(&match_value.value, TypeContext::default()); + } + ast::Pattern::MatchClass(match_class) => { + let ast::PatternMatchClass { + range: _, + node_index: _, + cls, + arguments, + } = match_class; + for pattern in &arguments.patterns { + self.infer_nested_match_pattern(pattern); + } + for keyword in &arguments.keywords { + self.infer_nested_match_pattern(&keyword.pattern); + } + let cls_ty = self.infer_standalone_expression(cls, TypeContext::default()); + self.validate_class_pattern(match_class, cls_ty); + } + ast::Pattern::MatchOr(match_or) => { + for pattern in &match_or.patterns { + self.infer_match_pattern(pattern); + } + } + _ => { + self.infer_nested_match_pattern(pattern); } } - - self.infer_body(body); } - fn infer_with_item_definition( - &mut self, - with_item: &WithItemDefinitionKind<'db>, - definition: Definition<'db>, - ) { - let context_expr = with_item.context_expr(self.module()); - let target = with_item.target(self.module()); - - let target_ty = match with_item.target_kind() { - TargetKind::Sequence(unpack_position, unpack) => { - let unpacked = infer_unpack_types(self.db(), unpack); - if unpack_position == UnpackPosition::First { - self.context.extend(unpacked.diagnostics()); + fn infer_nested_match_pattern(&mut self, pattern: &ast::Pattern) { + match pattern { + ast::Pattern::MatchValue(match_value) => { + self.infer_maybe_standalone_expression(&match_value.value, TypeContext::default()); + } + ast::Pattern::MatchSequence(match_sequence) => { + for pattern in &match_sequence.patterns { + self.infer_nested_match_pattern(pattern); } - unpacked.expression_type(target) } - TargetKind::Single => { - let context_expr_ty = - self.infer_standalone_expression(context_expr, TypeContext::default()); - self.infer_context_expression(context_expr, context_expr_ty, with_item.is_async()) + ast::Pattern::MatchMapping(match_mapping) => { + let ast::PatternMatchMapping { + range: _, + node_index: _, + keys, + patterns, + rest: _, + } = match_mapping; + for key in keys { + self.infer_expression(key, TypeContext::default()); + } + for pattern in patterns { + self.infer_nested_match_pattern(pattern); + } } - }; + ast::Pattern::MatchClass(match_class) => { + let ast::PatternMatchClass { + range: _, + node_index: _, + cls, + arguments, + } = match_class; + for pattern in &arguments.patterns { + self.infer_nested_match_pattern(pattern); + } + for keyword in &arguments.keywords { + self.infer_nested_match_pattern(&keyword.pattern); + } + let cls_ty = self.infer_maybe_standalone_expression(cls, TypeContext::default()); + self.validate_class_pattern(match_class, cls_ty); + } + ast::Pattern::MatchAs(match_as) => { + if let Some(pattern) = &match_as.pattern { + self.infer_nested_match_pattern(pattern); + } + } + ast::Pattern::MatchOr(match_or) => { + for pattern in &match_or.patterns { + self.infer_nested_match_pattern(pattern); + } + } + ast::Pattern::MatchStar(_) | ast::Pattern::MatchSingleton(_) => {} + } + } - self.store_expression_type(target, target_ty); - self.add_binding(target.into(), definition) - .insert(self, target_ty); + fn infer_assignment_statement(&mut self, assignment: &ast::StmtAssign) { + let ast::StmtAssign { + range: _, + node_index: _, + targets, + value, + } = assignment; + + for target in targets { + self.infer_target(target, value, &|builder, tcx| { + builder.infer_standalone_expression(value, tcx) + }); + } } - /// Infers the type of a context expression (`with expr`) and returns the target's type + /// Infer the (definition) types involved in a `target` expression. /// - /// Returns [`Type::unknown`] if the context expression doesn't implement the context manager protocol. + /// This is used for assignment statements, for statements, etc. with a single or multiple + /// targets (unpacking). If `target` is an attribute expression, we check that the assignment + /// is valid. For 'target's that are definitions, this check happens elsewhere. /// - /// ## Terminology - /// See [PEP343](https://peps.python.org/pep-0343/#standard-terminology). - fn infer_context_expression( + /// The `infer_value_expr` function is used to infer the type of the `value` expression which + /// are not `Name` expressions. The returned type is the one that is eventually assigned to the + /// `target`. + fn infer_target( &mut self, - context_expression: &ast::Expr, - context_expression_type: Type<'db>, - is_async: bool, - ) -> Type<'db> { - let eval_mode = if is_async { - EvaluationMode::Async - } else { - EvaluationMode::Sync - }; + target: &ast::Expr, + value: &ast::Expr, + infer_value_expr: &dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>, + ) { + match target { + ast::Expr::Name(_) => { + self.infer_target_impl(target, value, None); + } - context_expression_type - .try_enter_with_mode(self.db(), eval_mode) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - context_expression_type, - context_expression.into(), - ); - err.fallback_enter_type(self.db()) - }) + _ => self.infer_target_impl(target, value, Some(&infer_value_expr)), + } } - fn infer_exception(&mut self, node: Option<&ast::Expr>, is_star: bool) -> Type<'db> { - // If there is no handled exception, it's invalid syntax; - // a diagnostic will have already been emitted - let node_ty = node.map_or(Type::unknown(), |ty| { - self.infer_expression(ty, TypeContext::default()) - }); - let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db()); - - // If it's an `except*` handler, this won't actually be the type of the bound symbol; - // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. - let symbol_ty = if let Some(tuple_spec) = node_ty.tuple_instance_spec(self.db()) { - let mut builder = UnionBuilder::new(self.db()); - let mut invalid_elements = vec![]; + /// Make sure that the attribute assignment `obj.attribute = value` is valid. + /// + /// `target` is the node for the left-hand side, `object_ty` is the type of `obj`, `attribute` is + /// the name of the attribute being assigned, and `value_ty` is the type of the right-hand side of + /// the assignment. If the assignment is invalid, emit diagnostics. + fn validate_attribute_assignment( + &mut self, + target: &ast::ExprAttribute, + object_ty: Type<'db>, + attribute: &str, + infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, + emit_diagnostics: bool, + ) -> bool { + let db = self.db(); - for (index, element) in tuple_spec.all_elements().iter().enumerate() { - builder = builder.add( - if element.is_assignable_to(self.db(), type_base_exception) { - element.to_instance(self.db()).expect( - "`Type::to_instance()` should always return `Some()` \ - if called on a type assignable to `type[BaseException]`", - ) - } else { - invalid_elements.push((index, element)); - Type::unknown() - }, - ); - } + let mut first_tcx = None; - if !invalid_elements.is_empty() - && let Some(node) = node - { - if let ast::Expr::Tuple(tuple) = node - && !tuple.iter().any(ast::Expr::is_starred_expr) - && Some(tuple.len()) == tuple_spec.len().into_fixed_length() - { - let invalid_elements = invalid_elements - .iter() - .map(|(index, ty)| (&tuple.elts[*index], **ty)); - - report_invalid_exception_tuple_caught( - &self.context, - tuple, - node_ty, - invalid_elements, - ); - } else { - report_invalid_exception_caught(&self.context, node, node_ty); - } - } - - builder.build() - } else if node_ty.is_assignable_to(self.db(), type_base_exception) { - node_ty.to_instance(self.db()).expect( - "`Type::to_instance()` should always return `Some()` \ - if called on a type assignable to `type[BaseException]`", - ) - } else if node_ty.is_assignable_to( - self.db(), - Type::homogeneous_tuple(self.db(), type_base_exception), - ) { - node_ty - .tuple_instance_spec(self.db()) - .and_then(|spec| { - let specialization = spec - .homogeneous_element_type(self.db()) - .to_instance(self.db()); - - debug_assert!(specialization.is_some_and(|specialization_type| { - specialization_type.is_assignable_to( - self.db(), - KnownClass::BaseException.to_instance(self.db()), - ) - })); + // A wrapper over `infer_value_ty` that allows inferring the value type multiple times + // during attribute resolution. + let pure_infer_value_ty = infer_value_ty; + let mut infer_value_ty = |builder: &mut Self, tcx: TypeContext<'db>| -> Type<'db> { + // Overwrite the previously inferred value, preferring later inferences, which are + // likely more precise. Note that we still ensure each inference is assignable to + // its declared type, so this mainly affects the IDE hover type. + let prev_multi_inference_state = + builder.set_multi_inference_state(MultiInferenceState::Overwrite); - specialization - }) - .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db())) - } else if node_ty.is_assignable_to( - self.db(), - UnionType::from_two_elements( - self.db(), - type_base_exception, - Type::homogeneous_tuple(self.db(), type_base_exception), - ), - ) { - KnownClass::BaseException.to_instance(self.db()) - } else { - if let Some(node) = node { - report_invalid_exception_caught(&self.context, node, node_ty); - } - Type::unknown() - }; + // If we are inferring the argument multiple times, silence diagnostics to avoid duplicated warnings. + let was_in_multi_inference = if let Some(first_tcx) = first_tcx { + // The first time we infer an argument during multi-inference must be without type context, + // to avoid leaking diagnostics for bidirectional inference attempts. + debug_assert_eq!(first_tcx, TypeContext::default()); - if is_star { - let class = if symbol_ty - .is_subtype_of(self.db(), KnownClass::Exception.to_instance(self.db())) - { - KnownClass::ExceptionGroup + builder.context.set_multi_inference(true) } else { - KnownClass::BaseExceptionGroup + builder.context.is_in_multi_inference() }; - class.to_specialized_instance(self.db(), &[symbol_ty]) - } else { - symbol_ty - } - } - fn infer_except_handler_definition( - &mut self, - except_handler_definition: &ExceptHandlerDefinitionKind, - definition: Definition<'db>, - ) { - let symbol_ty = self.infer_exception( - except_handler_definition.handled_exceptions(self.module()), - except_handler_definition.is_star(), - ); + let value_ty = pure_infer_value_ty(builder, tcx); - self.add_binding( - except_handler_definition.node(self.module()).into(), - definition, - ) - .insert(self, symbol_ty); - } + // Reset the multi-inference state. + first_tcx.get_or_insert(tcx); + builder.multi_inference_state = prev_multi_inference_state; + builder.context.set_multi_inference(was_in_multi_inference); - fn infer_typevar_definition( - &mut self, - node: &ast::TypeParamTypeVar, - definition: Definition<'db>, - ) { - let ast::TypeParamTypeVar { - range: _, - node_index: _, - name, - bound, - default, - } = node; - - let bound_or_constraint = match bound.as_deref() { - Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { - if elts.len() < 2 { - if let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, expr) - { - builder.into_diagnostic("TypeVar must have at least two constrained types"); - } - None - } else { - Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints) - } - } - Some(_) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), - None => None, + value_ty }; - if bound_or_constraint.is_some() || default.is_some() { - self.deferred.insert(definition, self.multi_inference_state); - } - let identity = - TypeVarIdentity::new(self.db(), &name.id, Some(definition), TypeVarKind::Pep695); - let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( - self.db(), - identity, - bound_or_constraint, - None, // explicit_variance - default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy), - ))); - self.add_declaration_with_binding( - node.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(ty), - ); - } - - fn infer_typevar_deferred(&mut self, node: &'ast ast::TypeParamTypeVar) { - let ast::TypeParamTypeVar { - range: _, - node_index: _, - name, - bound, - default, - } = node; - let previous_deferred_state = - std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred); - let bound_node = bound.as_deref(); - let bound_or_constraints = match bound_node { - Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { - // Here, we interpret `bound` as a heterogeneous tuple and convert it to `TypeVarConstraints` - // in `TypeVarInstance::lazy_constraints`. - let constraint_tys: Box<[Type<'_>]> = elts - .iter() - .map(|expr| { - let constraint = self.infer_type_expression(expr); - if constraint.has_typevar_or_typevar_instance(self.db()) - && let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, expr) - { - builder.into_diagnostic("TypeVar constraint cannot be generic"); - } - constraint - }) - .collect(); - let tuple_ty = Type::heterogeneous_tuple(self.db(), constraint_tys.clone()); - self.store_expression_type(expr, tuple_ty); - // Mirror the `< 2` guard from `infer_typevar_definition` to avoid - // a cascading `invalid-type-variable-default` diagnostic for tuples - // that have already been flagged as invalid constraints. - if elts.len() < 2 { - None - } else { - Some(TypeVarBoundOrConstraints::Constraints( - TypeVarConstraints::new(self.db(), constraint_tys), - )) - } - } - Some(expr) => { - let bound_ty = self.infer_type_expression(expr); - if bound_ty.has_typevar_or_typevar_instance(self.db()) - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_VARIABLE_BOUND, expr) - { - builder.into_diagnostic("TypeVar upper bound cannot be generic"); + // This closure should only be called if `value_ty` was inferred with `attr_ty` as type context. + let ensure_assignable_to = + |builder: &Self, value_ty: Type<'db>, attr_ty: Type<'db>| -> bool { + let assignable = value_ty.is_assignable_to(db, attr_ty); + if !assignable && emit_diagnostics { + report_invalid_attribute_assignment( + &builder.context, + target.into(), + attr_ty, + value_ty, + attribute, + ); } + assignable + }; - Some(TypeVarBoundOrConstraints::UpperBound(bound_ty)) + let emit_invalid_final = |builder: &Self| { + if emit_diagnostics + && let Some(builder) = builder.context.report_lint(&INVALID_ASSIGNMENT, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to final attribute `{attribute}` on type `{}`", + object_ty.display(db) + )); } - None => None, }; - if let Some(default_expr) = default.as_deref() { - let default_ty = self.infer_type_expression(default_expr); - if !self.check_default_for_outer_scope_typevars(default_ty, default_expr, &name.id) { - let bound_node = bound_node.map(|n| match n { - ast::Expr::Tuple(tuple) => BoundOrConstraintsNodes::Constraints(&tuple.elts), - _ => BoundOrConstraintsNodes::Bound(n), - }); - self.validate_typevar_default( - Some(&name.id), - bound_or_constraints, - default_ty, - default_expr, - bound_node, - ); + + // Return true (and emit a diagnostic) if this is an invalid assignment to a `Final` attribute. + // Per PEP 591 and the typing conformance suite, Final instance attributes can be assigned + // in __init__ methods. Multiple assignments within __init__ are allowed (matching mypy + // and pyright behavior), as long as the attribute doesn't have a class-level value. + let invalid_assignment_to_final = |builder: &Self, qualifiers: TypeQualifiers| -> bool { + // Check if it's a Final attribute + if !qualifiers.contains(TypeQualifiers::FINAL) { + return false; } - } - self.deferred_state = previous_deferred_state; - } - /// Validate that a `TypeVar`'s default is compatible with its bound or constraints. - fn validate_typevar_default( - &mut self, - name: Option<&str>, - bound_or_constraints: Option>, - default_ty: Type<'db>, - default_node: &ast::Expr, - bound_or_constraints_nodes: Option>, - ) { - let Some(bound_or_constraints) = bound_or_constraints else { - return; - }; + // Check if we're in an __init__ method (where Final attributes can be initialized). + let is_in_init = builder + .current_function_definition() + .is_some_and(|func| func.name.id == "__init__"); - let db = self.db(); + // Not in __init__ - always disallow + if !is_in_init { + emit_invalid_final(builder); + return true; + } - // Normalize both typevar representations into a `TypeVarInstance` so they - // follow the same compatibility rules: - // - `Type::KnownInstance(TypeVar(..))` for legacy `typing.TypeVar(...)` values - // - `Type::TypeVar(..)` for bound in-scope type parameters (for example, PEP 695) - let default_typevar = match default_ty { - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Some(typevar), - Type::TypeVar(bound_typevar) => Some(bound_typevar.typevar(db)), - _ => None, - }; + // We're in __init__ - verify we're in a method of the class being mutated + let Some(class_ty) = builder.class_context_of_current_method() else { + // Not a method (standalone function named __init__) + emit_invalid_final(builder); + return true; + }; - let not_assignable_message = - "TypeVar default is not assignable to the TypeVar's upper bound"; + // Check that object_ty is an instance of the class we're in + if !object_ty.is_subtype_of(builder.db(), Type::instance(builder.db(), class_ty)) { + // Assigning to a different class's Final attribute + emit_invalid_final(builder); + return true; + } - let not_assignable_to_upper_bound = || { - self.context - .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_node) - .map(|builder| { - let mut diagnostic = builder.into_diagnostic(not_assignable_message); - if let Some(BoundOrConstraintsNodes::Bound(bound)) = bound_or_constraints_nodes - { - let secondary = self.context.secondary(bound); - let secondary = if let Some(name) = name { - secondary.message(format_args!("Upper bound of `{name}`")) - } else { - secondary.message("Upper bound of outer TypeVar") - }; - diagnostic.annotate(secondary); - } - diagnostic - }) - }; + // Check if class-level attribute already has a value + if let Some((class_literal, _)) = class_ty.static_class_literal(db) { + let class_scope_id = class_literal.body_scope(db).file_scope_id(db); + let place_table = builder.index.place_table(class_scope_id); - let inconsistent_with_constraints = || { - self.context - .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_node) - .map(|builder| { - let mut diagnostic = builder.into_diagnostic( - "TypeVar default is inconsistent \ - with the TypeVar's constraints", - ); - if let Some(BoundOrConstraintsNodes::Constraints([first, .., last])) = - bound_or_constraints_nodes + if let Some(symbol) = place_table.symbol_by_name(attribute) + && symbol.is_bound() + { + if emit_diagnostics + && let Some(diag_builder) = + builder.context.report_lint(&INVALID_ASSIGNMENT, target) { - let secondary = self - .context - .secondary(TextRange::new(first.start(), last.end())); - let secondary = if let Some(name) = name { - secondary.message(format_args!("Constraints of `{name}`")) - } else { - secondary.message("Constraints of outer TypeVar") - }; - diagnostic.annotate(secondary); + diag_builder.into_diagnostic(format_args!( + "Cannot assign to final attribute `{attribute}` in `__init__` \ + because it already has a value at class level" + )); } - diagnostic - }) - }; - - if let Some(default_typevar) = default_typevar { - let default_name = default_typevar.name(db); - // Annotate the diagnostic with the definition span of the default TypeVar. - let annotate_default_definition = |diagnostic: &mut LintDiagnosticGuard<'_, '_>| { - if let Some(definition) = default_typevar.definition(db) { - let file = definition.file(db); - diagnostic.annotate( - Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), - )) - .message(format_args!("`{default_name}` defined here")), - ); + return true; } - }; + } - match bound_or_constraints { - TypeVarBoundOrConstraints::UpperBound(outer_bound) => { - // Default TypeVar's upper bound must be assignable to outer's bound. - // If the default has constraints, all constraints must be assignable - // to the outer bound. - if let Some(default_constraints) = default_typevar.constraints(db) { - for constraint in default_constraints { - if !constraint.is_assignable_to(db, outer_bound) { - if let Some(mut diagnostic) = not_assignable_to_upper_bound() { - annotate_default_definition(&mut diagnostic); - if let Some(name) = name { - diagnostic.set_primary_message(format_args!( - "Constraint `{constraint}` of default \ - `{default_name}` is not assignable to upper \ - bound of `{name}`", - constraint = constraint.display(db), - )); - diagnostic.set_concise_message(format_args!( - "Default `{default_name}` of TypeVar `{name}` \ - is not assignable to upper bound `{bound}` \ - of `{name}` because constraint `{constraint}` \ - of `{default_name}` is not assignable to \ - `{bound}`", - bound = outer_bound.display(db), - constraint = constraint.display(db), - )); - } else { - diagnostic.set_primary_message(format_args!( - "Constraint `{constraint}` of `{default_name}` is \ - not assignable to upper bound `{bound}` of \ - outer TypeVar", - constraint = constraint.display(db), - bound = outer_bound.display(db), - )); - diagnostic.set_concise_message(format_args!( - "Default of TypeVar is not assignable its upper \ - bound `{bound}` because constraint `{constraint}` \ - of `{default_name}` is not assignable to `{bound}`", - bound = outer_bound.display(db), - constraint = constraint.display(db), - )); - } - } - break; - } - } - } else { - let default_bound = - default_typevar.upper_bound(db).unwrap_or_else(Type::object); - if !default_bound.is_assignable_to(db, outer_bound) { - if let Some(mut diagnostic) = not_assignable_to_upper_bound() { - annotate_default_definition(&mut diagnostic); - if let Some(name) = name { - diagnostic.set_primary_message(format_args!( - "Upper bound `{default_bound}` of default \ - `{default_name}` is not assignable to upper \ - bound of `{name}`", - default_bound = default_bound.display(db), - )); - diagnostic.set_concise_message(format_args!( - "Default `{default_name}` of TypeVar `{name}` \ - is not assignable to upper bound `{bound}` \ - of `{name}` because its upper bound \ - `{default_bound}` is not assignable to \ - `{bound}`", - bound = outer_bound.display(db), - default_bound = default_bound.display(db), - )); - } else { - diagnostic.set_primary_message(format_args!( - "Upper bound `{default_bound}` of default \ - `{default_name}` is not assignable to upper \ - bound of outer TypeVar", - default_bound = default_bound.display(db), - )); - diagnostic.set_concise_message(format_args!( - "TypeVar default `{default_name}` is not \ - assignable to upper bound `{bound}` \ - because upper bound of `{default_name}` - (`{default_bound}`) is not assignable - to `{bound}`", - bound = outer_bound.display(db), - default_bound = default_bound.display(db), - )); - } - } - } - } - } - TypeVarBoundOrConstraints::Constraints(outer_constraints) => { - // TypeVar default with constrained outer. - let outer = outer_constraints.elements(db); - if let Some(default_constraints) = default_typevar.constraints(db) { - // Default has constraints: outer constraints must be a superset. - for default_constraint in default_constraints { - if !outer - .iter() - .any(|o| default_constraint.is_equivalent_to(db, *o)) - { - if let Some(mut diagnostic) = inconsistent_with_constraints() { - annotate_default_definition(&mut diagnostic); - if let Some(name) = name { - diagnostic.set_primary_message(format_args!( - "Constraint `{constraint}` of default \ - `{default_name}` is not one of the constraints \ - of `{name}`", - constraint = default_constraint.display(db), - )); - diagnostic.set_concise_message(format_args!( - "Default `{default_name}` of TypeVar `{name}` \ - is inconsistent with its constraints \ - `{name}` because constraint `{constraint}` of \ - `{default_name}` is not one of the constraints \ - of `{name}`", - constraint = default_constraint.display(db), - )); - } else { - diagnostic.set_primary_message(format_args!( - "Constraint `{constraint}` of outer TypeVar default \ - `{default_name}` is not one of the constraints \ - of the outer TypeVar", - constraint = default_constraint.display(db), - )); - diagnostic.set_concise_message(format_args!( - "Default `{default_name}` of outer TypeVar is \ - inconsistent with the constraints of the outer \ - TypeVar because constraint `{constraint}` of \ - default `{default_name}` is not one of the \ - constraints of the outer TypeVar", - constraint = default_constraint.display(db), - )); - } - } - break; - } - } - } else { - // A non-constrained default TypeVar (bounded or unbounded) is - // incompatible with a constrained outer TypeVar per the typing spec. - if let Some(mut diagnostic) = inconsistent_with_constraints() { - annotate_default_definition(&mut diagnostic); - if let Some(default_bound) = default_typevar.upper_bound(db) { - diagnostic.set_primary_message( - "Bounded TypeVar cannot be used as the default \ - for a constrained TypeVar", - ); - diagnostic.info(format_args!( - "`{default_name}` has bound `{default_bound}` but is not constrained", - default_bound = default_bound.display(db), - )); - } else { - diagnostic.set_primary_message( - "Unbounded TypeVar cannot be used as the default \ - for a constrained TypeVar", - ); - diagnostic.info(format_args!( - "`{default_name}` has no bound or constraints", - )); - } - } - } - } - } - return; - } + // In __init__ and no class-level value - allow + false + }; - // Concrete default type checks. - match bound_or_constraints { - TypeVarBoundOrConstraints::UpperBound(bound) => { - if !default_ty.is_assignable_to(db, bound) { - if let Some(mut diagnostic) = not_assignable_to_upper_bound() { - if let Some(name) = name { - diagnostic.set_primary_message(format_args!("Default of `{name}`")); - } else { - diagnostic.set_primary_message("TypeVar default"); - } - diagnostic.set_concise_message(not_assignable_message); - } - } - } - TypeVarBoundOrConstraints::Constraints(constraints) => { - if default_ty != Type::any() - && !constraints - .elements(db) - .iter() - .any(|c| default_ty.is_equivalent_to(db, *c)) - { - if let Some(mut diagnostic) = inconsistent_with_constraints() { - if let Some(name) = name { - diagnostic.set_primary_message(format_args!( - "`{default}` is not one of the constraints of `{name}`", - default = default_ty.display(db), - )); - } else { - diagnostic.set_primary_message(format_args!( - "`{default}` is not one of the constraints", - default = default_ty.display(db), - )); - } + match object_ty { + Type::Union(union) => { + // First infer the value without type context, and then again for each union element. + let value_ty = infer_value_ty(self, TypeContext::default()); + + if union.elements(self.db()).iter().all(|elem| { + self.validate_attribute_assignment( + target, + *elem, + attribute, + // Note that `infer_value_ty` silences diagnostics after the first inference. + &mut infer_value_ty, + false, + ) + }) { + true + } else { + // TODO: This is not a very helpful error message, as it does not include the underlying reason + // why the assignment is invalid. This would be a good use case for sub-diagnostics. + if emit_diagnostics + && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + builder.into_diagnostic(format_args!( + "Object of type `{}` is not assignable \ + to attribute `{attribute}` on type `{}`", + value_ty.display(self.db()), + object_ty.display(self.db()), + )); } + + false } } - } - } - /// Check if a PEP 695 type parameter's default references type variables from an outer scope. - /// - /// Returns `true` if such a reference was found and a diagnostic was emitted, - /// indicating that further default validation should be skipped. - /// - /// Note: this only handles PEP 695 type parameters in function and type alias scopes. - /// Class type parameter scopes are skipped here because out-of-scope references - /// are validated at the class level via `report_invalid_typevar_default_reference`. - /// Legacy `TypeVar`s are validated by `check_legacy_typevar_defaults`. - fn check_default_for_outer_scope_typevars( - &self, - default_ty: Type<'db>, - default_node: &ast::Expr, - typevar_name: &str, - ) -> bool { - let db = self.db(); + Type::Intersection(intersection) => { + // First infer the value without type context, and then again for each union element. + let value_ty = infer_value_ty(self, TypeContext::default()); - // Determine the expected binding context from the current type parameter scope. - // Only check function and type alias scopes; class scopes are handled separately - // when processing the class definition. - let expected_binding_def = match self.scope().node(db) { - NodeWithScopeKind::FunctionTypeParameters(function) => { - self.index.expect_single_definition(function) - } - NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => { - self.index.expect_single_definition(type_alias) - } - _ => return false, - }; - let expected_binding = BindingContext::Definition(expected_binding_def); + // TODO: Handle negative intersection elements + if intersection.positive(db).iter().any(|elem| { + self.validate_attribute_assignment( + target, + *elem, + attribute, + // Note that `infer_value_ty` silences diagnostics after the first inference. + &mut infer_value_ty, + false, + ) + }) { + true + } else { + if emit_diagnostics + && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + // TODO: same here, see above + builder.into_diagnostic(format_args!( + "Object of type `{}` is not assignable \ + to attribute `{attribute}` on type `{}`", + value_ty.display(self.db()), + object_ty.display(self.db()), + )); + } - let outer_tv = find_over_type(db, default_ty, false, |ty| { - if let Type::TypeVar(bound_tv) = ty - && bound_tv.binding_context(db) != expected_binding - { - Some(bound_tv) - } else { - None + false + } } - }); - - let Some(outer_tv) = outer_tv else { - return false; - }; - let outer_typevar = outer_tv.typevar(db); - let outer_name = outer_typevar.name(db); - let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_node) - else { - return false; - }; - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid default for type parameter `{typevar_name}`" - )); - diagnostic.set_primary_message(format_args!( - "`{outer_name}` is a type parameter bound in an outer scope" - )); - diagnostic.set_concise_message(format_args!( - "Type parameter `{typevar_name}` cannot use \ - outer-scope type parameter `{outer_name}` as its default" - )); - if let Some(definition) = outer_typevar.definition(db) { - let file = definition.file(db); - diagnostic.annotate( - Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), - )) - .message(format_args!("`{outer_name}` defined here")), - ); - } - diagnostic.info("See https://typing.python.org/en/latest/spec/generics.html#scoping-rules"); - - true - } - - fn infer_paramspec_definition( - &mut self, - node: &ast::TypeParamParamSpec, - definition: Definition<'db>, - ) { - let ast::TypeParamParamSpec { - range: _, - node_index: _, - name, - default, - } = node; - if default.is_some() { - self.deferred.insert(definition, self.multi_inference_state); - } - let identity = TypeVarIdentity::new( - self.db(), - &name.id, - Some(definition), - TypeVarKind::Pep695ParamSpec, - ); - let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( - self.db(), - identity, - None, // ParamSpec, when declared using PEP 695 syntax, has no bounds or constraints - None, // explicit_variance - default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy), - ))); - self.add_declaration_with_binding( - node.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(ty), - ); - } - fn infer_paramspec_deferred(&mut self, node: &ast::TypeParamParamSpec) { - let ast::TypeParamParamSpec { - range: _, - node_index: _, - name, - default: Some(default), - } = node - else { - return; - }; - let previous_deferred_state = - std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred); - self.infer_paramspec_default(default, Some(&name.id)); - self.deferred_state = previous_deferred_state; - } + Type::TypeAlias(alias) => self.validate_attribute_assignment( + target, + alias.value_type(self.db()), + attribute, + pure_infer_value_ty, + emit_diagnostics, + ), - fn infer_paramspec_default(&mut self, default_expr: &ast::Expr, paramspec_name: Option<&str>) { - let previously_allowed_paramspec = self - .inference_flags - .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true); - self.infer_paramspec_default_impl(default_expr, paramspec_name); - self.inference_flags.set( - InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, - previously_allowed_paramspec, - ); - } + // Super instances do not allow attribute assignment + Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Super) => { + infer_value_ty(self, TypeContext::default()); - fn infer_paramspec_default_impl( - &mut self, - default_expr: &ast::Expr, - paramspec_name: Option<&str>, - ) { - match default_expr { - ast::Expr::EllipsisLiteral(ellipsis) => { - let ty = self.infer_ellipsis_literal_expression(ellipsis); - self.store_expression_type(default_expr, ty); - return; - } - ast::Expr::List(ast::ExprList { elts, .. }) => { - let types = elts - .iter() - .map(|elt| self.infer_type_expression(elt)) - .collect::>(); - // N.B. We cannot represent a heterogeneous list of types in our type system, so we - // use a heterogeneous tuple type to represent the list of types instead. - self.store_expression_type( - default_expr, - Type::heterogeneous_tuple(self.db(), types), - ); - return; - } - ast::Expr::Name(_) => { - let ty = self.infer_type_expression(default_expr); - if let Some(name) = paramspec_name - && self.check_default_for_outer_scope_typevars(ty, default_expr, name) + if emit_diagnostics + && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { - return; - } - let is_paramspec = match ty { - Type::TypeVar(typevar) => typevar.is_paramspec(self.db()), - Type::KnownInstance(known_instance) => { - known_instance.class(self.db()) == KnownClass::ParamSpec - } - _ => false, - }; - if is_paramspec { - return; + builder.into_diagnostic(format_args!( + "Cannot assign to attribute `{attribute}` on type `{}`", + object_ty.display(self.db()), + )); } - } - _ => {} - } - if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, default_expr) { - builder.into_diagnostic( - "The default value to `ParamSpec` must be either \ - a list of types, `ParamSpec`, or `...`", - ); - } - } - - /// Infer the type of the expression that represents an explicit specialization of a - /// `ParamSpec` type variable. - fn infer_paramspec_explicit_specialization_value( - &mut self, - expr: &ast::Expr, - exactly_one_paramspec: bool, - ) -> Result, ()> { - let db = self.db(); - - match expr { - ast::Expr::EllipsisLiteral(_) => { - return Ok(Type::paramspec_value_callable( - db, - Parameters::gradual_form(), - )); - } - ast::Expr::Tuple(_) if !exactly_one_paramspec => { - // Tuple expression is only allowed when the generic context contains only one - // `ParamSpec` type variable and no other type variables. + false } + Type::BoundSuper(_) => { + infer_value_ty(self, TypeContext::default()); - ast::Expr::Tuple(ast::ExprTuple { elts, .. }) - | ast::Expr::List(ast::ExprList { elts, .. }) => { - let mut parameter_types = Vec::with_capacity(elts.len()); - - // Whether to infer `Todo` for the parameters - let mut return_todo = false; - - for param in elts { - let param_type = self.infer_type_expression(param); - // This is similar to what we currently do for inferring tuple type expression. - // We currently infer `Todo` for the parameters to avoid invalid diagnostics - // when trying to check for assignability or any other relation. For example, - // `*tuple[int, str]`, `Unpack[]`, etc. are not yet supported. - return_todo |= param_type.is_todo() - && matches!(param, ast::Expr::Starred(_) | ast::Expr::Subscript(_)); - parameter_types.push(param_type); + if emit_diagnostics + && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to attribute `{attribute}` on type `{}`", + object_ty.display(self.db()), + )); } - - let parameters = if return_todo { - // TODO: `Unpack` - Parameters::todo() - } else { - Parameters::new( - self.db(), - parameter_types.iter().map(|param_type| { - Parameter::positional_only(None).with_annotated_type(*param_type) - }), - ) - }; - - return Ok(Type::paramspec_value_callable(db, parameters)); + false } - ast::Expr::Subscript(_) => { - // TODO: Support `Concatenate[...]` - return Ok(Type::paramspec_value_callable(db, Parameters::todo())); + Type::Dynamic(..) | Type::Never => { + infer_value_ty(self, TypeContext::default()); + true } - ast::Expr::Name(name) => { - if name.is_invalid() { - return Err(()); - } + Type::NominalInstance(..) + | Type::ProtocolInstance(_) + | Type::LiteralValue(..) + | Type::SpecialForm(..) + | Type::KnownInstance(..) + | Type::PropertyInstance(..) + | Type::FunctionLiteral(..) + | Type::Callable(..) + | Type::BoundMethod(_) + | Type::KnownBoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::TypeVar(..) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::TypeIs(_) + | Type::TypeGuard(_) + | Type::TypedDict(_) + | Type::NewTypeInstance(_) => { + // TODO: We could use the annotated parameter type of `__setattr__` as type context here. + // However, we would still have to perform the first inference without type context. + let value_ty = infer_value_ty(self, TypeContext::default()); - let param_type = self.infer_type_expression(expr); + // Infer `__setattr__` once upfront. We use this result for: + // 1. Checking if it returns `Never` (indicating an immutable class) + // 2. As a fallback when no explicit attribute is found + let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy( + db, + "__setattr__", + &mut CallArguments::positional([Type::string_literal(db, attribute), value_ty]), + TypeContext::default(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ); - match param_type { - Type::TypeVar(typevar) if typevar.is_paramspec(db) => { - return Ok(param_type); - } + // Check if `__setattr__` returns `Never` (indicating an immutable class). + // If so, block all attribute assignments regardless of explicit attributes. + let setattr_returns_never = match &setattr_dunder_call_result { + Ok(result) => result.return_type(db).is_never(), + Err(err) => err.return_type(db).is_some_and(|ty| ty.is_never()), + }; - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) - if typevar.is_paramspec(db) => - { - if let Some(diagnostic_builder) = - self.context.report_lint(&INVALID_TYPE_ARGUMENTS, expr) + if setattr_returns_never { + if emit_diagnostics { + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { - diagnostic_builder.into_diagnostic(format_args!( - "ParamSpec `{}` is unbound", - typevar.name(self.db()) - )); - } - return Err(()); - } + let is_setattr_synthesized = match object_ty.class_member_with_policy( + db, + "__setattr__".into(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) { + PlaceAndQualifiers { + place: Place::Defined(DefinedPlace { ty: attr_ty, .. }), + qualifiers: _, + } => attr_ty.is_callable_type(), + _ => false, + }; - // This is to handle the following case: - // - // ```python - // from typing import ParamSpec - // - // class Foo[**P]: ... - // - // Foo[ParamSpec] # P: (ParamSpec, /) - // ``` - Type::NominalInstance(nominal) - if nominal.has_known_class(self.db(), KnownClass::ParamSpec) => - { - return Ok(Type::paramspec_value_callable( - db, - Parameters::new( - self.db(), - [ - Parameter::positional_only(None) - .with_annotated_type(param_type), - ], - ), - )); - } + let member_exists = + !object_ty.member(db, attribute).place.is_undefined(); - _ if exactly_one_paramspec => { - // Square brackets are optional when `ParamSpec` is the only type variable - // being specialized. This means that a single name expression represents a - // parameter list with a single parameter. For example, - // - // ```python - // class OnlyParamSpec[**P]: ... - // - // OnlyParamSpec[int] # P: (int, /) - // ``` - let parameters = - if param_type.is_todo() { - Parameters::todo() + let msg = if !member_exists { + format!( + "Cannot assign to unresolved attribute `{attribute}` on type `{}`", + object_ty.display(db) + ) + } else if is_setattr_synthesized { + format!( + "Property `{attribute}` defined in `{}` is read-only", + object_ty.display(db) + ) } else { - Parameters::new( - self.db(), - [Parameter::positional_only(None) - .with_annotated_type(param_type)], + format!( + "Cannot assign to attribute `{attribute}` on type `{}` \ + whose `__setattr__` method returns `Never`/`NoReturn`", + object_ty.display(db) ) }; - return Ok(Type::paramspec_value_callable(db, parameters)); - } - // This is specifically to handle a case where there are more than one type - // variables and at least one of them is a `ParamSpec` which is specialized - // using `typing.Any`. This isn't explicitly allowed in the spec, but both mypy - // and Pyright allows this and the ecosystem report suggested there are usages - // of this in the wild e.g., `staticmethod[Any, Any]`. For example, - // - // ```python - // class Foo[**P, T]: ... - // - // Foo[Any, int] # P: (Any, /), T: int - // ``` - Type::Dynamic(DynamicType::Any) => { - return Ok(Type::paramspec_value_callable( - db, - Parameters::gradual_form(), - )); + builder.into_diagnostic(msg); + } } - - _ => {} + return false; } - } - _ => {} - } - - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, expr) { - builder.into_diagnostic( - "Type argument for `ParamSpec` must be either \ - a list of types, `ParamSpec`, `Concatenate`, or `...`", - ); - } - - Err(()) - } + // Now check for explicit attributes (class member or instance member). + // If an explicit attribute exists, validate against its type. + // Only fall back to `__setattr__` when no explicit attribute is found. + match object_ty.class_member(db, attribute.into()) { + meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => { + if emit_diagnostics + && let Some(builder) = + self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to ClassVar `{attribute}` \ + from an instance of type `{ty}`", + ty = object_ty.display(self.db()), + )); + } + false + } + PlaceAndQualifiers { + place: + Place::Defined(DefinedPlace { + ty: meta_attr_ty, + definedness: meta_attr_boundness, + .. + }), + qualifiers, + } => { + // Resolve `Self` type variables to the concrete instance type. + let meta_attr_ty = meta_attr_ty.bind_self_typevars(db, object_ty); - fn infer_typevartuple_definition( - &mut self, - node: &ast::TypeParamTypeVarTuple, - definition: Definition<'db>, - ) { - let ast::TypeParamTypeVarTuple { - range: _, - node_index: _, - name: _, - default, - } = node; - self.infer_optional_expression(default.as_deref(), TypeContext::default()); - let pep_695_todo = todo_type!("PEP-695 TypeVarTuple definition types"); - self.add_declaration_with_binding( - node.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(pep_695_todo), - ); - } + if invalid_assignment_to_final(self, qualifiers) { + return false; + } - /// Infer the type for a loop header definition. - /// - /// The loop header sees all the bindings that originate in the loop and are visible at a - /// loop-back edge (either the end of the loop body or a `continue` statement). See `struct - /// LoopHeader` in the semantic index for more on how all this fits together. - fn infer_loop_header_definition( - &mut self, - loop_header_kind: &LoopHeaderDefinitionKind<'db>, - definition: Definition<'db>, - ) { - let db = self.db(); - let place = loop_header_kind.place(); - let use_def = self - .index - .use_def_map(self.scope().file_scope_id(self.db())); - let loop_header = loop_header_reachability(db, definition); + let assignable_to_meta_attr = if let Place::Defined(DefinedPlace { + ty: meta_dunder_set, + .. + }) = + meta_attr_ty.class_member(db, "__set__".into()).place + { + // TODO: We could use the annotated parameter type of `__set__` as + // type context here. + let dunder_set_result = meta_dunder_set.try_call( + db, + &CallArguments::positional([meta_attr_ty, object_ty, value_ty]), + ); - let mut union = UnionBuilder::new(db).recursively_defined(RecursivelyDefined::Yes); + if emit_diagnostics + && let Err(dunder_set_failure) = dunder_set_result.as_ref() + { + report_bad_dunder_set_call( + &self.context, + dunder_set_failure, + attribute, + object_ty, + target, + ); + } - for reachable_binding in &loop_header.reachable_bindings { - let binding_ty = binding_type(db, reachable_binding.definition); - let narrowed_ty = use_def - .narrowing_evaluator(reachable_binding.narrowing_constraint) - .narrow(db, binding_ty, place); + dunder_set_result.is_ok() + } else { + let value_ty = + infer_value_ty(self, TypeContext::new(Some(meta_attr_ty))); - union.add_in_place(narrowed_ty); - } + ensure_assignable_to(self, value_ty, meta_attr_ty) + }; - self.bindings - .insert(definition, union.build(), self.multi_inference_state); - } + let assignable_to_instance_attribute = if meta_attr_boundness + == Definedness::PossiblyUndefined + { + let (assignable, boundness) = if let PlaceAndQualifiers { + place: + Place::Defined(DefinedPlace { + ty: instance_attr_ty, + definedness: instance_attr_boundness, + .. + }), + qualifiers, + } = + object_ty.instance_member(db, attribute) + { + // Bind `Self` via MRO matching. + let instance_attr_ty = + instance_attr_ty.bind_self_typevars(db, object_ty); + let value_ty = + infer_value_ty(self, TypeContext::new(Some(instance_attr_ty))); + if invalid_assignment_to_final(self, qualifiers) { + return false; + } - fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { - let ast::StmtMatch { - range: _, - node_index: _, - subject, - cases, - } = match_statement; + ( + ensure_assignable_to(self, value_ty, instance_attr_ty), + instance_attr_boundness, + ) + } else { + (true, Definedness::PossiblyUndefined) + }; - self.infer_standalone_expression(subject, TypeContext::default()); + if boundness == Definedness::PossiblyUndefined { + report_possibly_missing_attribute( + &self.context, + target, + attribute, + object_ty, + ); + } - for case in cases { - let ast::MatchCase { - range: _, - node_index: _, - body, - pattern, - guard, - } = case; - self.infer_match_pattern(pattern); + assignable + } else { + true + }; - if let Some(guard) = guard.as_deref() { - let guard_ty = self.infer_standalone_expression(guard, TypeContext::default()); + assignable_to_meta_attr && assignable_to_instance_attribute + } - if let Err(err) = guard_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, guard); - } - } + PlaceAndQualifiers { + place: Place::Undefined, + .. + } => { + if let PlaceAndQualifiers { + place: + Place::Defined(DefinedPlace { + ty: instance_attr_ty, + definedness: instance_attr_boundness, + .. + }), + qualifiers, + } = object_ty.instance_member(db, attribute) + { + // Bind `Self` via MRO matching. + let instance_attr_ty = + instance_attr_ty.bind_self_typevars(db, object_ty); + let value_ty = + infer_value_ty(self, TypeContext::new(Some(instance_attr_ty))); + if invalid_assignment_to_final(self, qualifiers) { + return false; + } - self.infer_body(body); - } - } - - fn infer_match_pattern_definition( - &mut self, - pattern: &'ast ast::Pattern, - _index: u32, - definition: Definition<'db>, - ) { - // TODO(dhruvmanila): The correct way to infer types here is to perform structural matching - // against the subject expression type (which we can query via `infer_expression_types`) - // and extract the type at the `index` position if the pattern matches. This will be - // similar to the logic in `self.infer_assignment_definition`. - self.add_binding(pattern.into(), definition) - .insert(self, todo_type!("`match` pattern definition types")); - } - - fn validate_class_pattern(&mut self, pattern: &ast::PatternMatchClass, cls_ty: Type<'db>) { - if let Type::ClassLiteral(class) = cls_ty { - if class.is_typed_dict(self.db()) { - report_match_pattern_against_typed_dict(&self.context, &*pattern.cls, class); - } else if let Some(protocol_class) = class.into_protocol_class(self.db()) - && !protocol_class.is_runtime_checkable(self.db()) - { - report_match_pattern_against_non_runtime_checkable_protocol( - &self.context, - &*pattern.cls, - protocol_class, - ); - } - } else if !cls_ty.is_assignable_to(self.db(), KnownClass::Type.to_instance(self.db())) { - report_invalid_class_match_pattern(&self.context, &*pattern.cls, cls_ty); - } - } - - fn infer_match_pattern(&mut self, pattern: &ast::Pattern) { - // We need to create a standalone expression for each arm of a match statement, since they - // can introduce constraints on the match subject. (Or more accurately, for the match arm's - // pattern, since its the pattern that introduces any constraints, not the body.) Ideally, - // that standalone expression would wrap the match arm's pattern as a whole. But a - // standalone expression can currently only wrap an ast::Expr, which patterns are not. So, - // we need to choose an Expr that can “stand in” for the pattern, which we can wrap in a - // standalone expression. - // - // That said, when inferring the type of a standalone expression, we don't have access to - // its parent or sibling nodes. That means, for instance, that in a class pattern, where - // we are currently using the class name as the standalone expression, we do not have - // access to the class pattern's arguments in the standalone expression inference scope. - // At the moment, we aren't trying to do anything with those arguments when creating a - // narrowing constraint for the pattern. But in the future, if we do, we will have to - // either wrap those arguments in their own standalone expressions, or update Expression to - // be able to wrap other AST node types besides just ast::Expr. - // - // This function is only called for the top-level pattern of a match arm, and is - // responsible for inferring the standalone expression for each supported pattern type. It - // then hands off to `infer_nested_match_pattern` for any subexpressions and subpatterns, - // where we do NOT have any additional standalone expressions to infer through. - // - // TODO(dhruvmanila): Add a Salsa query for inferring pattern types and matching against - // the subject expression: https://github.com/astral-sh/ruff/pull/13147#discussion_r1739424510 - match pattern { - ast::Pattern::MatchValue(match_value) => { - self.infer_standalone_expression(&match_value.value, TypeContext::default()); - } - ast::Pattern::MatchClass(match_class) => { - let ast::PatternMatchClass { - range: _, - node_index: _, - cls, - arguments, - } = match_class; - for pattern in &arguments.patterns { - self.infer_nested_match_pattern(pattern); - } - for keyword in &arguments.keywords { - self.infer_nested_match_pattern(&keyword.pattern); - } - let cls_ty = self.infer_standalone_expression(cls, TypeContext::default()); - self.validate_class_pattern(match_class, cls_ty); - } - ast::Pattern::MatchOr(match_or) => { - for pattern in &match_or.patterns { - self.infer_match_pattern(pattern); - } - } - _ => { - self.infer_nested_match_pattern(pattern); - } - } - } - - fn infer_nested_match_pattern(&mut self, pattern: &ast::Pattern) { - match pattern { - ast::Pattern::MatchValue(match_value) => { - self.infer_maybe_standalone_expression(&match_value.value, TypeContext::default()); - } - ast::Pattern::MatchSequence(match_sequence) => { - for pattern in &match_sequence.patterns { - self.infer_nested_match_pattern(pattern); - } - } - ast::Pattern::MatchMapping(match_mapping) => { - let ast::PatternMatchMapping { - range: _, - node_index: _, - keys, - patterns, - rest: _, - } = match_mapping; - for key in keys { - self.infer_expression(key, TypeContext::default()); - } - for pattern in patterns { - self.infer_nested_match_pattern(pattern); - } - } - ast::Pattern::MatchClass(match_class) => { - let ast::PatternMatchClass { - range: _, - node_index: _, - cls, - arguments, - } = match_class; - for pattern in &arguments.patterns { - self.infer_nested_match_pattern(pattern); - } - for keyword in &arguments.keywords { - self.infer_nested_match_pattern(&keyword.pattern); - } - let cls_ty = self.infer_maybe_standalone_expression(cls, TypeContext::default()); - self.validate_class_pattern(match_class, cls_ty); - } - ast::Pattern::MatchAs(match_as) => { - if let Some(pattern) = &match_as.pattern { - self.infer_nested_match_pattern(pattern); - } - } - ast::Pattern::MatchOr(match_or) => { - for pattern in &match_or.patterns { - self.infer_nested_match_pattern(pattern); - } - } - ast::Pattern::MatchStar(_) | ast::Pattern::MatchSingleton(_) => {} - } - } - - fn infer_assignment_statement(&mut self, assignment: &ast::StmtAssign) { - let ast::StmtAssign { - range: _, - node_index: _, - targets, - value, - } = assignment; - - for target in targets { - self.infer_target(target, value, &|builder, tcx| { - builder.infer_standalone_expression(value, tcx) - }); - } - } - - /// Infer the (definition) types involved in a `target` expression. - /// - /// This is used for assignment statements, for statements, etc. with a single or multiple - /// targets (unpacking). If `target` is an attribute expression, we check that the assignment - /// is valid. For 'target's that are definitions, this check happens elsewhere. - /// - /// The `infer_value_expr` function is used to infer the type of the `value` expression which - /// are not `Name` expressions. The returned type is the one that is eventually assigned to the - /// `target`. - fn infer_target( - &mut self, - target: &ast::Expr, - value: &ast::Expr, - infer_value_expr: &dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>, - ) { - match target { - ast::Expr::Name(_) => { - self.infer_target_impl(target, value, None); - } - - _ => self.infer_target_impl(target, value, Some(&infer_value_expr)), - } - } - - /// Validate a subscript assignment of the form `object[key] = rhs_value`. - fn validate_subscript_assignment( - &mut self, - target: &ast::ExprSubscript, - rhs_value: &ast::Expr, - infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, - ) -> bool { - let ast::ExprSubscript { - range: _, - node_index: _, - value: object, - slice, - ctx: _, - } = target; - - let object_ty = self.infer_expression(object, TypeContext::default()); - let mut infer_slice_ty = |builder: &mut Self, tcx| builder.infer_expression(slice, tcx); - - self.validate_subscript_assignment_impl( - target, - None, - object_ty, - &mut infer_slice_ty, - rhs_value, - infer_rhs_value, - true, - ) - } - - #[expect(clippy::too_many_arguments)] - fn validate_subscript_assignment_impl( - &mut self, - target: &ast::ExprSubscript, - full_object_ty: Option>, - object_ty: Type<'db>, - infer_slice_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, - rhs_value_node: &ast::Expr, - infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, - emit_diagnostic: bool, - ) -> bool { - /// Given a string literal or a union of string literals, return an iterator over the contained - /// strings, or `None`, if the type is neither. - fn key_literals<'db>( - db: &'db dyn Db, - slice_ty: Type<'db>, - ) -> Option + 'db> { - if let Some(literal) = slice_ty.as_string_literal() { - Some(Either::Left(std::iter::once(literal.value(db)))) - } else { - slice_ty.as_union().map(|union| { - Either::Right( - union - .elements(db) - .iter() - .filter_map(|ty| ty.as_string_literal().map(|lit| lit.value(db))), - ) - }) - } - } - - let db = self.db(); - - let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { - if let Some(full_object_ty) = full_object_ty { - diagnostic.info(format_args!( - "The full type of the subscripted object is `{}`", - full_object_ty.display(db) - )); - } - }; - - match object_ty { - Type::Union(union) => { - // TODO: Perform multi-inference here. - let slice_ty = infer_slice_ty(self, TypeContext::default()); - let rhs_value_ty = infer_rhs_value(self, TypeContext::default()); - - // Note that we use a loop here instead of .all(…) to avoid short-circuiting. - // We need to keep iterating to emit all diagnostics. - let mut valid = true; - for element_ty in union.elements(db) { - valid &= self.validate_subscript_assignment_impl( - target, - full_object_ty.or(Some(object_ty)), - *element_ty, - &mut |_, _| slice_ty, - rhs_value_node, - &mut |_, _| rhs_value_ty, - emit_diagnostic, - ); - } - valid - } - - Type::Intersection(intersection) => { - // TODO: Perform multi-inference here. - let slice_ty = infer_slice_ty(self, TypeContext::default()); - let rhs_value_ty = infer_rhs_value(self, TypeContext::default()); - - let mut check_positive_elements = |emit_diagnostic_and_short_circuit| { - let mut valid = false; - for element_ty in intersection.positive(db) { - valid |= self.validate_subscript_assignment_impl( - target, - full_object_ty.or(Some(object_ty)), - *element_ty, - &mut |_, _| slice_ty, - rhs_value_node, - &mut |_, _| rhs_value_ty, - emit_diagnostic_and_short_circuit, - ); - - if !valid && emit_diagnostic_and_short_circuit { - break; - } - } - - valid - }; - - // Perform an initial check of all elements. If the assignment is valid - // for at least one element, we do not emit any diagnostics. Otherwise, - // we re-run the check and emit a diagnostic on the first failing element. - let valid = check_positive_elements(false); - - if !valid { - check_positive_elements(true); - } - - valid - } - - Type::TypedDict(typed_dict) => { - // As an optimization, prevent calling `__setitem__` on (unions of) large `TypedDict`s, and - // validate the assignment ourselves. This also allows us to emit better diagnostics. - - // TODO: Use type context here. - let slice_ty = infer_slice_ty(self, TypeContext::default()); - let rhs_value_ty = infer_rhs_value(self, TypeContext::default()); - - let mut valid = true; - let Some(keys) = key_literals(db, slice_ty) else { - // Check if the key has a valid type. We only allow string literals, a union of string literals, - // or a dynamic type like `Any`. We can do this by checking assignability to `LiteralString`, - // but we need to exclude `LiteralString` itself. This check would technically allow weird key - // types like `LiteralString & Any` to pass, but it does not need to be perfect. We would just - // fail to provide the "can only be subscripted with a string literal key" hint in that case. - - if slice_ty.is_dynamic() { - return true; - } - - let assigned_d = rhs_value_ty.display(db); - let value_d = object_ty.display(db); - - if slice_ty.is_assignable_to(db, Type::literal_string()) - && !slice_ty.is_equivalent_to(db, Type::literal_string()) - { - if let Some(builder) = self - .context - .report_lint(&INVALID_ASSIGNMENT, target.slice.as_ref()) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot assign value of type `{assigned_d}` to key of type `{}` on TypedDict `{value_d}`", - slice_ty.display(db) - )); - attach_original_type_info(&mut diagnostic); - } - } else { - if let Some(builder) = self - .context - .report_lint(&INVALID_KEY, target.slice.as_ref()) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "TypedDict `{value_d}` can only be subscripted with a string literal key, got key of type `{}`.", - slice_ty.display(db) - )); - attach_original_type_info(&mut diagnostic); - } - } - - return false; - }; - - for key in keys { - valid &= TypedDictKeyAssignment { - context: &self.context, - typed_dict, - full_object_ty, - key, - value_ty: rhs_value_ty, - typed_dict_node: target.value.as_ref().into(), - key_node: target.slice.as_ref().into(), - value_node: rhs_value_node.into(), - assignment_kind: TypedDictAssignmentKind::Subscript, - emit_diagnostic, - } - .validate(); - } - - valid - } - - _ => { - let ast_arguments = [ - ArgOrKeyword::Arg(&target.slice), - ArgOrKeyword::Arg(rhs_value_node), - ]; - - let mut call_arguments = - CallArguments::positional([Type::unknown(), Type::unknown()]); - - let mut infer_argument_ty = - |builder: &mut Self, (argument_index, _, tcx): ArgExpr<'db, '_>| { - match argument_index { - 0 => infer_slice_ty(builder, tcx), - 1 => infer_rhs_value(builder, tcx), - _ => unreachable!(), - } - }; - - let Err(call_dunder_err) = self.infer_and_try_call_dunder( - db, - object_ty, - "__setitem__", - ArgumentsIter::synthesized(&ast_arguments), - &mut call_arguments, - &mut infer_argument_ty, - TypeContext::default(), - ) else { - return true; - }; - - let [Some(slice_ty), Some(rhs_value_ty)] = call_arguments.types() else { - unreachable!(); - }; - - match call_dunder_err { - CallDunderError::PossiblyUnbound { .. } => { - if emit_diagnostic - && let Some(builder) = self - .context - .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__setitem__` of type `{}` may be missing", - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - false - } - CallDunderError::CallError(call_error_kind, bindings) => { - match call_error_kind { - CallErrorKind::NotCallable => { - if emit_diagnostic - && let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__setitem__` of type `{}` is not callable \ - on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - } - CallErrorKind::BindingError => { - if let Some(typed_dict) = object_ty.as_typed_dict() { - if let Some(key) = slice_ty.as_string_literal() { - let key = key.value(db); - TypedDictKeyAssignment { - context: &self.context, - typed_dict, - full_object_ty, - key, - value_ty: *rhs_value_ty, - typed_dict_node: target.value.as_ref().into(), - key_node: target.slice.as_ref().into(), - value_node: rhs_value_node.into(), - assignment_kind: TypedDictAssignmentKind::Subscript, - emit_diagnostic: true, - } - .validate(); - } - } else { - if emit_diagnostic - && let Some(builder) = self.context.report_lint( - &INVALID_ASSIGNMENT, - target.range.cover(rhs_value_node.range()), - ) - { - let assigned_d = rhs_value_ty.display(db); - let object_d = object_ty.display(db); - - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid subscript assignment with key of type `{}` and value of \ - type `{assigned_d}` on object of type `{object_d}`", - slice_ty.display(db), - )); - - // Special diagnostic for dictionaries - if let Some([expected_key_ty, expected_value_ty]) = - object_ty - .known_specialization(db, KnownClass::Dict) - .map(|s| s.types(db)) - { - if !slice_ty.is_assignable_to(db, *expected_key_ty) { - diagnostic.annotate( - self.context - .secondary(target.slice.as_ref()) - .message(format_args!( - "Expected key of type `{}`, got `{}`", - expected_key_ty.display(db), - slice_ty.display(db), - )), - ); - } - - if !rhs_value_ty - .is_assignable_to(db, *expected_value_ty) - { - diagnostic.annotate( - self.context.secondary(rhs_value_node).message( - format_args!( - "Expected value of type `{}`, got `{}`", - expected_value_ty.display(db), - rhs_value_ty.display(db), - ), - ), - ); - } - } - - attach_original_type_info(&mut diagnostic); - } - } - } - CallErrorKind::PossiblyNotCallable => { - if emit_diagnostic - && let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__setitem__` of type `{}` may not be callable on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - } - } - false - } - CallDunderError::MethodNotAvailable => { - if emit_diagnostic - && let Some(builder) = - self.context.report_lint(&INVALID_ASSIGNMENT, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot assign to a subscript on an object of type `{}`", - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - - // If it's a user-defined class, suggest adding a `__setitem__` method. - if object_ty - .as_nominal_instance() - .and_then(|instance| instance.class(db).static_class_literal(db)) - .and_then(|(class_literal, _)| { - file_to_module(db, class_literal.file(db)) - }) - .and_then(|module| module.search_path(db)) - .is_some_and(ty_module_resolver::SearchPath::is_first_party) - { - diagnostic.help(format_args!( - "Consider adding a `__setitem__` method to `{}`.", - object_ty.display(db), - )); - } else { - diagnostic.info(format_args!( - "`{}` does not have a `__setitem__` method.", - object_ty.display(db), - )); - } - } - false - } - } - } - } - } - - /// Validate a subscript deletion of the form `del object[key]`. - fn validate_subscript_deletion( - &self, - target: &ast::ExprSubscript, - object_ty: Type<'db>, - slice_ty: Type<'db>, - ) { - self.validate_subscript_deletion_impl(target, None, object_ty, slice_ty); - } - - fn validate_subscript_deletion_impl( - &self, - target: &'ast ast::ExprSubscript, - full_object_ty: Option>, - object_ty: Type<'db>, - slice_ty: Type<'db>, - ) { - let db = self.db(); - - let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { - if let Some(full_object_ty) = full_object_ty { - diagnostic.info(format_args!( - "The full type of the subscripted object is `{}`", - full_object_ty.display(db) - )); - } - }; - - match object_ty { - Type::Union(union) => { - for element_ty in union.elements(db) { - self.validate_subscript_deletion_impl( - target, - full_object_ty.or(Some(object_ty)), - *element_ty, - slice_ty, - ); - } - } - - Type::Intersection(intersection) => { - // Check if any positive element supports deletion - let mut any_valid = false; - for element_ty in intersection.positive(db) { - if self.can_delete_subscript(*element_ty, slice_ty) { - any_valid = true; - break; - } - } - - // If none are valid, emit a diagnostic for the first failing element - if !any_valid && let Some(element_ty) = intersection.positive(db).first() { - self.validate_subscript_deletion_impl( - target, - full_object_ty.or(Some(object_ty)), - *element_ty, - slice_ty, - ); - } - } - - _ => { - match object_ty.try_call_dunder( - db, - "__delitem__", - CallArguments::positional([slice_ty]), - TypeContext::default(), - ) { - Ok(_) => {} - Err(err) => match err { - CallDunderError::PossiblyUnbound { .. } => { - if let Some(builder) = self - .context - .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` may be missing", - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - } - CallDunderError::CallError(call_error_kind, bindings) => { - match call_error_kind { - CallErrorKind::NotCallable => { - if let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` is not callable \ - on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - } - CallErrorKind::BindingError => { - // For deletions of string literal keys on `TypedDict`, provide - // a more detailed diagnostic. - if let Some(typed_dict) = object_ty.as_typed_dict() { - if let Some(string_literal) = slice_ty.as_string_literal() { - let key = string_literal.value(db); - let items = typed_dict.items(db); - - if let Some(field) = items.get(key) { - // Key exists but is required (i.e., can't be deleted). - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - object_ty, - key, - Some(field), - TypedDictDeleteErrorKind::RequiredKey, - ); - } else { - // Key doesn't exist. - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - object_ty, - key, - None, - TypedDictDeleteErrorKind::UnknownKey, - ); - } - } else { - // Non-string-literal key on `TypedDict`. - if let Some(builder) = self - .context - .report_lint(&INVALID_ARGUMENT_TYPE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` cannot be called \ - with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db), - slice_ty.display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - } - } else { - // Non-`TypedDict` object - if let Some(builder) = - self.context.report_lint(&INVALID_ARGUMENT_TYPE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` cannot be called \ - with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db), - slice_ty.display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - } - } - CallErrorKind::PossiblyNotCallable => { - if let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` may not be callable \ - on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - } - } - } - CallDunderError::MethodNotAvailable => { - report_not_subscriptable( - &self.context, - target, - object_ty, - "__delitem__", - ); - } - }, - } - } - } - } - - /// Check if a type supports subscript deletion (has `__delitem__`). - fn can_delete_subscript(&self, object_ty: Type<'db>, slice_ty: Type<'db>) -> bool { - let db = self.db(); - object_ty - .try_call_dunder( - db, - "__delitem__", - CallArguments::positional([slice_ty]), - TypeContext::default(), - ) - .is_ok() - } - - /// Make sure that the attribute assignment `obj.attribute = value` is valid. - /// - /// `target` is the node for the left-hand side, `object_ty` is the type of `obj`, `attribute` is - /// the name of the attribute being assigned, and `value_ty` is the type of the right-hand side of - /// the assignment. If the assignment is invalid, emit diagnostics. - fn validate_attribute_assignment( - &mut self, - target: &ast::ExprAttribute, - object_ty: Type<'db>, - attribute: &str, - infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, - emit_diagnostics: bool, - ) -> bool { - let db = self.db(); - - let mut first_tcx = None; - - // A wrapper over `infer_value_ty` that allows inferring the value type multiple times - // during attribute resolution. - let pure_infer_value_ty = infer_value_ty; - let mut infer_value_ty = |builder: &mut Self, tcx: TypeContext<'db>| -> Type<'db> { - // Overwrite the previously inferred value, preferring later inferences, which are - // likely more precise. Note that we still ensure each inference is assignable to - // its declared type, so this mainly affects the IDE hover type. - let prev_multi_inference_state = - builder.set_multi_inference_state(MultiInferenceState::Overwrite); - - // If we are inferring the argument multiple times, silence diagnostics to avoid duplicated warnings. - let was_in_multi_inference = if let Some(first_tcx) = first_tcx { - // The first time we infer an argument during multi-inference must be without type context, - // to avoid leaking diagnostics for bidirectional inference attempts. - debug_assert_eq!(first_tcx, TypeContext::default()); - - builder.context.set_multi_inference(true) - } else { - builder.context.is_in_multi_inference() - }; - - let value_ty = pure_infer_value_ty(builder, tcx); - - // Reset the multi-inference state. - first_tcx.get_or_insert(tcx); - builder.multi_inference_state = prev_multi_inference_state; - builder.context.set_multi_inference(was_in_multi_inference); - - value_ty - }; - - // This closure should only be called if `value_ty` was inferred with `attr_ty` as type context. - let ensure_assignable_to = - |builder: &Self, value_ty: Type<'db>, attr_ty: Type<'db>| -> bool { - let assignable = value_ty.is_assignable_to(db, attr_ty); - if !assignable && emit_diagnostics { - report_invalid_attribute_assignment( - &builder.context, - target.into(), - attr_ty, - value_ty, - attribute, - ); - } - assignable - }; - - let emit_invalid_final = |builder: &Self| { - if emit_diagnostics - && let Some(builder) = builder.context.report_lint(&INVALID_ASSIGNMENT, target) - { - builder.into_diagnostic(format_args!( - "Cannot assign to final attribute `{attribute}` on type `{}`", - object_ty.display(db) - )); - } - }; - - // Return true (and emit a diagnostic) if this is an invalid assignment to a `Final` attribute. - // Per PEP 591 and the typing conformance suite, Final instance attributes can be assigned - // in __init__ methods. Multiple assignments within __init__ are allowed (matching mypy - // and pyright behavior), as long as the attribute doesn't have a class-level value. - let invalid_assignment_to_final = |builder: &Self, qualifiers: TypeQualifiers| -> bool { - // Check if it's a Final attribute - if !qualifiers.contains(TypeQualifiers::FINAL) { - return false; - } - - // Check if we're in an __init__ method (where Final attributes can be initialized). - let is_in_init = builder - .current_function_definition() - .is_some_and(|func| func.name.id == "__init__"); - - // Not in __init__ - always disallow - if !is_in_init { - emit_invalid_final(builder); - return true; - } - - // We're in __init__ - verify we're in a method of the class being mutated - let Some(class_ty) = builder.class_context_of_current_method() else { - // Not a method (standalone function named __init__) - emit_invalid_final(builder); - return true; - }; - - // Check that object_ty is an instance of the class we're in - if !object_ty.is_subtype_of(builder.db(), Type::instance(builder.db(), class_ty)) { - // Assigning to a different class's Final attribute - emit_invalid_final(builder); - return true; - } - - // Check if class-level attribute already has a value - if let Some((class_literal, _)) = class_ty.static_class_literal(db) { - let class_scope_id = class_literal.body_scope(db).file_scope_id(db); - let place_table = builder.index.place_table(class_scope_id); - - if let Some(symbol) = place_table.symbol_by_name(attribute) - && symbol.is_bound() - { - if emit_diagnostics - && let Some(diag_builder) = - builder.context.report_lint(&INVALID_ASSIGNMENT, target) - { - diag_builder.into_diagnostic(format_args!( - "Cannot assign to final attribute `{attribute}` in `__init__` \ - because it already has a value at class level" - )); - } - - return true; - } - } - - // In __init__ and no class-level value - allow - false - }; - - match object_ty { - Type::Union(union) => { - // First infer the value without type context, and then again for each union element. - let value_ty = infer_value_ty(self, TypeContext::default()); - - if union.elements(self.db()).iter().all(|elem| { - self.validate_attribute_assignment( - target, - *elem, - attribute, - // Note that `infer_value_ty` silences diagnostics after the first inference. - &mut infer_value_ty, - false, - ) - }) { - true - } else { - // TODO: This is not a very helpful error message, as it does not include the underlying reason - // why the assignment is invalid. This would be a good use case for sub-diagnostics. - if emit_diagnostics - && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) - { - builder.into_diagnostic(format_args!( - "Object of type `{}` is not assignable \ - to attribute `{attribute}` on type `{}`", - value_ty.display(self.db()), - object_ty.display(self.db()), - )); - } - - false - } - } - - Type::Intersection(intersection) => { - // First infer the value without type context, and then again for each union element. - let value_ty = infer_value_ty(self, TypeContext::default()); - - // TODO: Handle negative intersection elements - if intersection.positive(db).iter().any(|elem| { - self.validate_attribute_assignment( - target, - *elem, - attribute, - // Note that `infer_value_ty` silences diagnostics after the first inference. - &mut infer_value_ty, - false, - ) - }) { - true - } else { - if emit_diagnostics - && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) - { - // TODO: same here, see above - builder.into_diagnostic(format_args!( - "Object of type `{}` is not assignable \ - to attribute `{attribute}` on type `{}`", - value_ty.display(self.db()), - object_ty.display(self.db()), - )); - } - - false - } - } - - Type::TypeAlias(alias) => self.validate_attribute_assignment( - target, - alias.value_type(self.db()), - attribute, - pure_infer_value_ty, - emit_diagnostics, - ), - - // Super instances do not allow attribute assignment - Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Super) => { - infer_value_ty(self, TypeContext::default()); - - if emit_diagnostics - && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) - { - builder.into_diagnostic(format_args!( - "Cannot assign to attribute `{attribute}` on type `{}`", - object_ty.display(self.db()), - )); - } - - false - } - Type::BoundSuper(_) => { - infer_value_ty(self, TypeContext::default()); - - if emit_diagnostics - && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) - { - builder.into_diagnostic(format_args!( - "Cannot assign to attribute `{attribute}` on type `{}`", - object_ty.display(self.db()), - )); - } - false - } - - Type::Dynamic(..) | Type::Never => { - infer_value_ty(self, TypeContext::default()); - true - } - - Type::NominalInstance(..) - | Type::ProtocolInstance(_) - | Type::LiteralValue(..) - | Type::SpecialForm(..) - | Type::KnownInstance(..) - | Type::PropertyInstance(..) - | Type::FunctionLiteral(..) - | Type::Callable(..) - | Type::BoundMethod(_) - | Type::KnownBoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::TypeVar(..) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::TypeIs(_) - | Type::TypeGuard(_) - | Type::TypedDict(_) - | Type::NewTypeInstance(_) => { - // TODO: We could use the annotated parameter type of `__setattr__` as type context here. - // However, we would still have to perform the first inference without type context. - let value_ty = infer_value_ty(self, TypeContext::default()); - - // Infer `__setattr__` once upfront. We use this result for: - // 1. Checking if it returns `Never` (indicating an immutable class) - // 2. As a fallback when no explicit attribute is found - let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy( - db, - "__setattr__", - &mut CallArguments::positional([Type::string_literal(db, attribute), value_ty]), - TypeContext::default(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ); - - // Check if `__setattr__` returns `Never` (indicating an immutable class). - // If so, block all attribute assignments regardless of explicit attributes. - let setattr_returns_never = match &setattr_dunder_call_result { - Ok(result) => result.return_type(db).is_never(), - Err(err) => err.return_type(db).is_some_and(|ty| ty.is_never()), - }; - - if setattr_returns_never { - if emit_diagnostics { - if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) - { - let is_setattr_synthesized = match object_ty.class_member_with_policy( - db, - "__setattr__".into(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ) { - PlaceAndQualifiers { - place: Place::Defined(DefinedPlace { ty: attr_ty, .. }), - qualifiers: _, - } => attr_ty.is_callable_type(), - _ => false, - }; - - let member_exists = - !object_ty.member(db, attribute).place.is_undefined(); - - let msg = if !member_exists { - format!( - "Cannot assign to unresolved attribute `{attribute}` on type `{}`", - object_ty.display(db) - ) - } else if is_setattr_synthesized { - format!( - "Property `{attribute}` defined in `{}` is read-only", - object_ty.display(db) - ) - } else { - format!( - "Cannot assign to attribute `{attribute}` on type `{}` \ - whose `__setattr__` method returns `Never`/`NoReturn`", - object_ty.display(db) - ) - }; - - builder.into_diagnostic(msg); - } - } - return false; - } - - // Now check for explicit attributes (class member or instance member). - // If an explicit attribute exists, validate against its type. - // Only fall back to `__setattr__` when no explicit attribute is found. - match object_ty.class_member(db, attribute.into()) { - meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => { - if emit_diagnostics - && let Some(builder) = - self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) - { - builder.into_diagnostic(format_args!( - "Cannot assign to ClassVar `{attribute}` \ - from an instance of type `{ty}`", - ty = object_ty.display(self.db()), - )); - } - false - } - PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - ty: meta_attr_ty, - definedness: meta_attr_boundness, - .. - }), - qualifiers, - } => { - // Resolve `Self` type variables to the concrete instance type. - let meta_attr_ty = meta_attr_ty.bind_self_typevars(db, object_ty); - - if invalid_assignment_to_final(self, qualifiers) { - return false; - } - - let assignable_to_meta_attr = if let Place::Defined(DefinedPlace { - ty: meta_dunder_set, - .. - }) = - meta_attr_ty.class_member(db, "__set__".into()).place - { - // TODO: We could use the annotated parameter type of `__set__` as - // type context here. - let dunder_set_result = meta_dunder_set.try_call( - db, - &CallArguments::positional([meta_attr_ty, object_ty, value_ty]), - ); - - if emit_diagnostics - && let Err(dunder_set_failure) = dunder_set_result.as_ref() - { - report_bad_dunder_set_call( - &self.context, - dunder_set_failure, - attribute, - object_ty, - target, - ); - } - - dunder_set_result.is_ok() - } else { - let value_ty = - infer_value_ty(self, TypeContext::new(Some(meta_attr_ty))); - - ensure_assignable_to(self, value_ty, meta_attr_ty) - }; - - let assignable_to_instance_attribute = if meta_attr_boundness - == Definedness::PossiblyUndefined - { - let (assignable, boundness) = if let PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - ty: instance_attr_ty, - definedness: instance_attr_boundness, - .. - }), - qualifiers, - } = - object_ty.instance_member(db, attribute) - { - // Bind `Self` via MRO matching. - let instance_attr_ty = - instance_attr_ty.bind_self_typevars(db, object_ty); - let value_ty = - infer_value_ty(self, TypeContext::new(Some(instance_attr_ty))); - if invalid_assignment_to_final(self, qualifiers) { - return false; - } - - ( - ensure_assignable_to(self, value_ty, instance_attr_ty), - instance_attr_boundness, - ) - } else { - (true, Definedness::PossiblyUndefined) - }; - - if boundness == Definedness::PossiblyUndefined { - report_possibly_missing_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - assignable - } else { - true - }; - - assignable_to_meta_attr && assignable_to_instance_attribute - } - - PlaceAndQualifiers { - place: Place::Undefined, - .. - } => { - if let PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - ty: instance_attr_ty, - definedness: instance_attr_boundness, - .. - }), - qualifiers, - } = object_ty.instance_member(db, attribute) - { - // Bind `Self` via MRO matching. - let instance_attr_ty = - instance_attr_ty.bind_self_typevars(db, object_ty); - let value_ty = - infer_value_ty(self, TypeContext::new(Some(instance_attr_ty))); - if invalid_assignment_to_final(self, qualifiers) { - return false; - } - - if instance_attr_boundness == Definedness::PossiblyUndefined { - report_possibly_missing_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - ensure_assignable_to(self, value_ty, instance_attr_ty) - } else { - // No explicit attribute found. Use `__setattr__` (already inferred - // above) as a fallback for dynamic attribute assignment. - match setattr_dunder_call_result { - // If __setattr__ succeeded, allow the assignment. - Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true, - Err(CallDunderError::CallError(..)) => { - if emit_diagnostics - && let Some(builder) = - self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) - { - builder.into_diagnostic(format_args!( - "Cannot assign object of type `{}` to attribute \ - `{attribute}` on type `{}` with \ - custom `__setattr__` method.", - value_ty.display(db), - object_ty.display(db) - )); - } - false - } - Err(CallDunderError::MethodNotAvailable) => { - if emit_diagnostics - && let Some(builder) = - self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) - { - builder.into_diagnostic(format_args!( - "Unresolved attribute `{}` on type `{}`", - attribute, - object_ty.display(db) - )); - } - false - } - } - } - } - } - } - - Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { - match object_ty.class_member(db, attribute.into()) { - PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - ty: meta_attr_ty, - definedness: meta_attr_boundness, - .. - }), - qualifiers, - } => { - // We may have to perform multi-inference if the meta attribute is possibly unbound. - // However, we are required to perform the first inference without type context. - let value_ty = infer_value_ty(self, TypeContext::default()); - - if invalid_assignment_to_final(self, qualifiers) { - return false; - } - - let assignable_to_meta_attr = if let Place::Defined(DefinedPlace { - ty: meta_dunder_set, - .. - }) = - meta_attr_ty.class_member(db, "__set__".into()).place - { - // TODO: We could use the annotated parameter type of `__set__` as - // type context here. - let dunder_set_result = meta_dunder_set.try_call( - db, - &CallArguments::positional([meta_attr_ty, object_ty, value_ty]), - ); - - if emit_diagnostics - && let Err(dunder_set_failure) = dunder_set_result.as_ref() - { - report_bad_dunder_set_call( - &self.context, - dunder_set_failure, - attribute, - object_ty, - target, - ); - } - - dunder_set_result.is_ok() - } else { - let value_ty = - infer_value_ty(self, TypeContext::new(Some(meta_attr_ty))); - ensure_assignable_to(self, value_ty, meta_attr_ty) - }; - - let assignable_to_class_attr = if meta_attr_boundness - == Definedness::PossiblyUndefined - { - let (assignable, boundness) = if let Place::Defined(DefinedPlace { - ty: class_attr_ty, - definedness: class_attr_boundness, - .. - }) = object_ty - .find_name_in_mro(db, attribute) - .expect("called on Type::ClassLiteral or Type::SubclassOf") - .place - { - let value_ty = - infer_value_ty(self, TypeContext::new(Some(class_attr_ty))); - ( - ensure_assignable_to(self, value_ty, class_attr_ty), - class_attr_boundness, - ) - } else { - (true, Definedness::PossiblyUndefined) - }; - - if boundness == Definedness::PossiblyUndefined { - report_possibly_missing_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - assignable - } else { - true - }; - - assignable_to_meta_attr && assignable_to_class_attr - } - PlaceAndQualifiers { - place: Place::Undefined, - .. - } => { - if let PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - ty: class_attr_ty, - definedness: class_attr_boundness, - .. - }), - qualifiers, - } = object_ty - .find_name_in_mro(db, attribute) - .expect("called on Type::ClassLiteral or Type::SubclassOf") - { - let value_ty = - infer_value_ty(self, TypeContext::new(Some(class_attr_ty))); - if invalid_assignment_to_final(self, qualifiers) { - return false; - } - - if class_attr_boundness == Definedness::PossiblyUndefined { - report_possibly_missing_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - ensure_assignable_to(self, value_ty, class_attr_ty) - } else { - infer_value_ty(self, TypeContext::default()); - - let attribute_is_bound_on_instance = - object_ty.to_instance(self.db()).is_some_and(|instance| { - !instance - .instance_member(self.db(), attribute) - .place - .is_undefined() - }); - - // Attribute is declared or bound on instance. Forbid access from the class object - if emit_diagnostics { - if attribute_is_bound_on_instance { - if let Some(builder) = - self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) - { - builder.into_diagnostic(format_args!( - "Cannot assign to instance attribute \ - `{attribute}` from the class object `{ty}`", - ty = object_ty.display(self.db()), - )); - } - } else { - if let Some(builder) = - self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) - { - builder.into_diagnostic(format_args!( - "Unresolved attribute `{}` on type `{}`.", - attribute, - object_ty.display(db) - )); - } - } + if instance_attr_boundness == Definedness::PossiblyUndefined { + report_possibly_missing_attribute( + &self.context, + target, + attribute, + object_ty, + ); } - false - } - } - } - } - - Type::ModuleLiteral(module) => { - let sym = if module - .module(db) - .known(db) - .is_some_and(KnownModule::is_builtins) - { - builtins_symbol(db, attribute) - } else { - module.static_member(db, attribute) - }; - if let Place::Defined(DefinedPlace { ty: attr_ty, .. }) = sym.place { - let value_ty = infer_value_ty(self, TypeContext::new(Some(attr_ty))); - - let assignable = value_ty.is_assignable_to(db, attr_ty); - if assignable { - true - } else { - if emit_diagnostics { - report_invalid_attribute_assignment( - &self.context, - target.into(), - attr_ty, - value_ty, - attribute, - ); - } - false - } - } else { - infer_value_ty(self, TypeContext::default()); - - if emit_diagnostics - && let Some(builder) = - self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) - { - builder.into_diagnostic(format_args!( - "Unresolved attribute `{}` on type `{}`.", - attribute, - object_ty.display(db) - )); - } - - false - } - } - } - } - - #[expect(clippy::type_complexity)] - fn infer_target_impl( - &mut self, - target: &ast::Expr, - value: &ast::Expr, - infer_assigned_ty: Option<&dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>>, - ) { - match target { - ast::Expr::Name(name) => { - if let Some(infer_assigned_ty) = infer_assigned_ty { - infer_assigned_ty(self, TypeContext::default()); - } - - self.infer_definition(name); - } - ast::Expr::Starred(ast::ExprStarred { - value: starred_value, - .. - }) => { - self.infer_target_impl(starred_value, value, infer_assigned_ty); - } - ast::Expr::List(ast::ExprList { elts, .. }) - | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { - let assigned_ty = infer_assigned_ty.map(|f| f(self, TypeContext::default())); - - if let Some(tuple_spec) = - assigned_ty.and_then(|ty| ty.tuple_instance_spec(self.db())) - { - let assigned_tys = tuple_spec.all_elements().to_vec(); - - for (i, element) in elts.iter().enumerate() { - match assigned_tys.get(i).copied() { - None => self.infer_target_impl(element, value, None), - Some(ty) => self.infer_target_impl(element, value, Some(&|_, _| ty)), - } - } - } else { - for element in elts { - self.infer_target_impl(element, value, None); - } - } - } - ast::Expr::Attribute( - attr_expr @ ast::ExprAttribute { - value: object, - ctx: ExprContext::Store, - attr, - .. - }, - ) => { - let object_ty = self.infer_expression(object, TypeContext::default()); - - if let Some(infer_assigned_ty) = infer_assigned_ty { - let infer_assigned_ty = &mut |builder: &mut Self, tcx| { - let assigned_ty = infer_assigned_ty(builder, tcx); - builder.store_expression_type(target, assigned_ty); - assigned_ty - }; - - self.validate_attribute_assignment( - attr_expr, - object_ty, - attr.id(), - infer_assigned_ty, - true, - ); - } - } - ast::Expr::Subscript(subscript_expr) => { - if let Some(infer_assigned_ty) = infer_assigned_ty { - let infer_assigned_ty = &mut |builder: &mut Self, tcx| { - let assigned_ty = infer_assigned_ty(builder, tcx); - builder.store_expression_type(target, assigned_ty); - assigned_ty - }; - - self.validate_subscript_assignment(subscript_expr, value, infer_assigned_ty); - } - } - - // TODO: Remove this once we handle all possible assignment targets. - _ => { - if let Some(infer_assigned_ty) = infer_assigned_ty { - infer_assigned_ty(self, TypeContext::default()); - } - - self.infer_expression(target, TypeContext::default()); - } - } - } - - fn infer_assignment_definition( - &mut self, - assignment: &AssignmentDefinitionKind<'db>, - definition: Definition<'db>, - ) { - let target = assignment.target(self.module()); - - let add = self.add_binding(target.into(), definition); - let target_ty = - self.infer_assignment_definition_impl(assignment, definition, add.type_context()); - self.store_expression_type(target, target_ty); - add.insert(self, target_ty); - } - - fn infer_assignment_definition_impl( - &mut self, - assignment: &AssignmentDefinitionKind<'db>, - definition: Definition<'db>, - tcx: TypeContext<'db>, - ) -> Type<'db> { - let value = assignment.value(self.module()); - let target = assignment.target(self.module()); - - let mut target_ty = match assignment.target_kind() { - TargetKind::Sequence(unpack_position, unpack) => { - let unpacked = infer_unpack_types(self.db(), unpack); - // Only copy the diagnostics if this is the first assignment to avoid duplicating the - // unpack assignments. - if unpack_position == UnpackPosition::First { - self.context.extend(unpacked.diagnostics()); - } - - unpacked.expression_type(target) - } - TargetKind::Single => { - // This could be an implicit type alias (OptionalList = list[T] | None). Use the definition - // of `OptionalList` as the binding context while inferring the RHS (`list[T] | None`), in - // order to bind `T` to `OptionalList`. - let previous_typevar_binding_context = - self.typevar_binding_context.replace(definition); - - let value_ty = if let Some(standalone_expression) = self.index.try_expression(value) - { - self.infer_standalone_expression_impl(value, standalone_expression, tcx) - } else if let ast::Expr::Call(call_expr) = value { - // If the RHS is not a standalone expression, this is a simple assignment - // (single target, no unpackings). That means it's a valid syntactic form - // for a legacy TypeVar creation; check for that. - let callable_type = self.infer_maybe_standalone_expression( - call_expr.func.as_ref(), - TypeContext::default(), - ); - - let ty = if let Some(namedtuple_kind) = - NamedTupleKind::from_type(self.db(), callable_type) - { - self.infer_namedtuple_call_expression( - call_expr, - Some(definition), - namedtuple_kind, - ) - } else { - match callable_type - .as_class_literal() - .and_then(|cls| cls.known(self.db())) - { - Some( - typevar_class @ (KnownClass::TypeVar - | KnownClass::ExtensionsTypeVar), - ) => self.infer_legacy_typevar( - target, - call_expr, - definition, - typevar_class, - ), - Some( - paramspec_class @ (KnownClass::ParamSpec - | KnownClass::ExtensionsParamSpec), - ) => self.infer_legacy_paramspec( - target, - call_expr, - definition, - paramspec_class, - ), - Some(KnownClass::NewType) => { - self.infer_newtype_expression(target, call_expr, definition) - } - Some(KnownClass::Type) => { - // Try to extract the dynamic class with definition. - // This returns `None` if it's not a three-arg call to `type()`, - // signalling that we must fall back to normal call inference. - self.infer_builtins_type_call(call_expr, Some(definition)) - } - Some(KnownClass::TypeAliasType) => { - self.infer_typealiastype_call(target, call_expr, definition) - } - Some(_) | None => { - self.infer_call_expression_impl(call_expr, callable_type, tcx) + ensure_assignable_to(self, value_ty, instance_attr_ty) + } else { + // No explicit attribute found. Use `__setattr__` (already inferred + // above) as a fallback for dynamic attribute assignment. + match setattr_dunder_call_result { + // If __setattr__ succeeded, allow the assignment. + Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true, + Err(CallDunderError::CallError(..)) => { + if emit_diagnostics + && let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign object of type `{}` to attribute \ + `{attribute}` on type `{}` with \ + custom `__setattr__` method.", + value_ty.display(db), + object_ty.display(db) + )); + } + false + } + Err(CallDunderError::MethodNotAvailable) => { + if emit_diagnostics + && let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( + "Unresolved attribute `{}` on type `{}`", + attribute, + object_ty.display(db) + )); + } + false + } } } - }; - - self.store_expression_type(value, ty); - ty - } else { - self.infer_expression(value, tcx) - }; - - self.typevar_binding_context = previous_typevar_binding_context; - - // `TYPE_CHECKING` is a special variable that should only be assigned `False` - // at runtime, but is always considered `True` in type checking. - // See mdtest/known_constants.md#user-defined-type_checking for details. - if target.as_name_expr().map(|name| name.id.as_str()) == Some("TYPE_CHECKING") { - if !matches!( - value.as_boolean_literal_expr(), - Some(ast::ExprBooleanLiteral { value: false, .. }) - ) { - report_invalid_type_checking_constant(&self.context, target.into()); } - Type::bool_literal(true) - } else if self.in_stub() && value.is_ellipsis_literal_expr() { - Type::unknown() - } else { - value_ty } } - }; - - if let Some(special_form) = target.as_name_expr().and_then(|name| { - SpecialFormType::try_from_file_and_name(self.db(), self.file(), &name.id) - }) { - target_ty = Type::SpecialForm(special_form); - } - - target_ty - } - - fn infer_legacy_paramspec( - &mut self, - target: &ast::Expr, - call_expr: &ast::ExprCall, - definition: Definition<'db>, - known_class: KnownClass, - ) -> Type<'db> { - fn error<'db>( - context: &InferContext<'db, '_>, - message: impl std::fmt::Display, - node: impl Ranged, - ) -> Type<'db> { - if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, node) { - builder.into_diagnostic(message); - } - // If the call doesn't create a valid paramspec, we'll emit diagnostics and fall back to - // just creating a regular instance of `typing.ParamSpec`. - KnownClass::ParamSpec.to_instance(context.db()) - } - - let db = self.db(); - let arguments = &call_expr.arguments; - let is_typing_extensions = known_class == KnownClass::ExtensionsParamSpec; - let assume_all_features = self.in_stub() || is_typing_extensions; - let python_version = Program::get(db).python_version(db); - let have_features_from = - |version: PythonVersion| assume_all_features || python_version >= version; - let mut default = None; - let mut name_param_ty = None; + Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { + match object_ty.class_member(db, attribute.into()) { + PlaceAndQualifiers { + place: + Place::Defined(DefinedPlace { + ty: meta_attr_ty, + definedness: meta_attr_boundness, + .. + }), + qualifiers, + } => { + // We may have to perform multi-inference if the meta attribute is possibly unbound. + // However, we are required to perform the first inference without type context. + let value_ty = infer_value_ty(self, TypeContext::default()); - if arguments.args.len() > 1 { - return error( - &self.context, - "`ParamSpec` can only have one positional argument", - call_expr, - ); - } + if invalid_assignment_to_final(self, qualifiers) { + return false; + } - if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { - return error( - &self.context, - "Starred arguments are not supported in `ParamSpec` creation", - starred, - ); - } + let assignable_to_meta_attr = if let Place::Defined(DefinedPlace { + ty: meta_dunder_set, + .. + }) = + meta_attr_ty.class_member(db, "__set__".into()).place + { + // TODO: We could use the annotated parameter type of `__set__` as + // type context here. + let dunder_set_result = meta_dunder_set.try_call( + db, + &CallArguments::positional([meta_attr_ty, object_ty, value_ty]), + ); - for kwarg in &arguments.keywords { - let Some(identifier) = kwarg.arg.as_ref() else { - return error( - &self.context, - "Starred arguments are not supported in `ParamSpec` creation", - kwarg, - ); - }; - match identifier.id().as_str() { - "name" => { - // Duplicate keyword argument is a syntax error, so we don't have to check if - // `name_param_ty.is_some()` here. - if !arguments.args.is_empty() { - return error( - &self.context, - "The `name` parameter of `ParamSpec` can only be provided once", - kwarg, - ); - } - name_param_ty = - Some(self.infer_expression(&kwarg.value, TypeContext::default())); - } - "bound" | "covariant" | "contravariant" | "infer_variance" => { - return error( - &self.context, - "The variance and bound arguments for `ParamSpec` do not have defined semantics yet", - call_expr, - ); - } - "default" => { - if !have_features_from(PythonVersion::PY313) { - // We don't return here; this error is informational since this will error - // at runtime, but the user's intent is plain, we may as well respect it. - error( - &self.context, - "The `default` parameter of `typing.ParamSpec` was added in Python 3.13", - kwarg, - ); - } - default = Some(TypeVarDefaultEvaluation::Lazy); - } - name => { - // We don't return here; this error is informational since this will error - // at runtime, but it will likely cause fewer cascading errors if we just - // ignore the unknown keyword and still understand as much of the typevar as we - // can. - error( - &self.context, - format_args!("Unknown keyword argument `{name}` in `ParamSpec` creation"), - kwarg, - ); - self.infer_expression(&kwarg.value, TypeContext::default()); - } - } - } + if emit_diagnostics + && let Err(dunder_set_failure) = dunder_set_result.as_ref() + { + report_bad_dunder_set_call( + &self.context, + dunder_set_failure, + attribute, + object_ty, + target, + ); + } - let Some(name_param_ty) = name_param_ty.or_else(|| { - arguments - .find_positional(0) - .map(|arg| self.infer_expression(arg, TypeContext::default())) - }) else { - return error( - &self.context, - "The `name` parameter of `ParamSpec` is required.", - call_expr, - ); - }; + dunder_set_result.is_ok() + } else { + let value_ty = + infer_value_ty(self, TypeContext::new(Some(meta_attr_ty))); + ensure_assignable_to(self, value_ty, meta_attr_ty) + }; - let Some(name_param) = name_param_ty.as_string_literal().map(|name| name.value(db)) else { - return error( - &self.context, - "The first argument to `ParamSpec` must be a string literal", - call_expr, - ); - }; + let assignable_to_class_attr = if meta_attr_boundness + == Definedness::PossiblyUndefined + { + let (assignable, boundness) = if let Place::Defined(DefinedPlace { + ty: class_attr_ty, + definedness: class_attr_boundness, + .. + }) = object_ty + .find_name_in_mro(db, attribute) + .expect("called on Type::ClassLiteral or Type::SubclassOf") + .place + { + let value_ty = + infer_value_ty(self, TypeContext::new(Some(class_attr_ty))); + ( + ensure_assignable_to(self, value_ty, class_attr_ty), + class_attr_boundness, + ) + } else { + (true, Definedness::PossiblyUndefined) + }; - let ast::Expr::Name(ast::ExprName { - id: target_name, .. - }) = target - else { - return error( - &self.context, - "A `ParamSpec` definition must be a simple variable assignment", - target, - ); - }; + if boundness == Definedness::PossiblyUndefined { + report_possibly_missing_attribute( + &self.context, + target, + attribute, + object_ty, + ); + } - if name_param != target_name { - return error( - &self.context, - format_args!( - "The name of a `ParamSpec` (`{name_param}`) must match \ - the name of the variable it is assigned to (`{target_name}`)" - ), - target, - ); - } + assignable + } else { + true + }; - if default.is_some() { - self.deferred.insert(definition, self.multi_inference_state); - } + assignable_to_meta_attr && assignable_to_class_attr + } + PlaceAndQualifiers { + place: Place::Undefined, + .. + } => { + if let PlaceAndQualifiers { + place: + Place::Defined(DefinedPlace { + ty: class_attr_ty, + definedness: class_attr_boundness, + .. + }), + qualifiers, + } = object_ty + .find_name_in_mro(db, attribute) + .expect("called on Type::ClassLiteral or Type::SubclassOf") + { + let value_ty = + infer_value_ty(self, TypeContext::new(Some(class_attr_ty))); + if invalid_assignment_to_final(self, qualifiers) { + return false; + } - let identity = - TypeVarIdentity::new(db, target_name, Some(definition), TypeVarKind::ParamSpec); - Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( - db, identity, None, None, default, - ))) - } + if class_attr_boundness == Definedness::PossiblyUndefined { + report_possibly_missing_attribute( + &self.context, + target, + attribute, + object_ty, + ); + } - fn infer_legacy_typevar( - &mut self, - target: &ast::Expr, - call_expr: &ast::ExprCall, - definition: Definition<'db>, - known_class: KnownClass, - ) -> Type<'db> { - fn error<'db>( - context: &InferContext<'db, '_>, - message: impl std::fmt::Display, - node: impl Ranged, - ) -> Type<'db> { - if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) { - builder.into_diagnostic(message); - } - // If the call doesn't create a valid typevar, we'll emit diagnostics and fall back to - // just creating a regular instance of `typing.TypeVar`. - KnownClass::TypeVar.to_instance(context.db()) - } + ensure_assignable_to(self, value_ty, class_attr_ty) + } else { + infer_value_ty(self, TypeContext::default()); - let db = self.db(); - let arguments = &call_expr.arguments; - let is_typing_extensions = known_class == KnownClass::ExtensionsTypeVar; - let assume_all_features = self.in_stub() || is_typing_extensions; - let python_version = Program::get(db).python_version(db); - let have_features_from = - |version: PythonVersion| assume_all_features || python_version >= version; - - let mut has_bound = false; - let mut default = None; - let mut covariant = false; - let mut contravariant = false; - let mut name_param_ty = None; + let attribute_is_bound_on_instance = + object_ty.to_instance(self.db()).is_some_and(|instance| { + !instance + .instance_member(self.db(), attribute) + .place + .is_undefined() + }); - if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { - return error( - &self.context, - "Starred arguments are not supported in `TypeVar` creation", - starred, - ); - } + // Attribute is declared or bound on instance. Forbid access from the class object + if emit_diagnostics { + if attribute_is_bound_on_instance { + if let Some(builder) = + self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to instance attribute \ + `{attribute}` from the class object `{ty}`", + ty = object_ty.display(self.db()), + )); + } + } else { + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( + "Unresolved attribute `{}` on type `{}`.", + attribute, + object_ty.display(db) + )); + } + } + } - for kwarg in &arguments.keywords { - let Some(identifier) = kwarg.arg.as_ref() else { - return error( - &self.context, - "Starred arguments are not supported in `TypeVar` creation", - kwarg, - ); - }; - match identifier.id().as_str() { - "name" => { - // Duplicate keyword argument is a syntax error, so we don't have to check if - // `name_param_ty.is_some()` here. - if !arguments.args.is_empty() { - return error( - &self.context, - "The `name` parameter of `TypeVar` can only be provided once.", - kwarg, - ); - } - name_param_ty = - Some(self.infer_expression(&kwarg.value, TypeContext::default())); - } - "bound" => has_bound = true, - "covariant" => { - match self - .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) - { - Truthiness::AlwaysTrue => covariant = true, - Truthiness::AlwaysFalse => {} - Truthiness::Ambiguous => { - return error( - &self.context, - "The `covariant` parameter of `TypeVar` \ - cannot have an ambiguous truthiness", - &kwarg.value, - ); + false } } } - "contravariant" => { - match self - .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) - { - Truthiness::AlwaysTrue => contravariant = true, - Truthiness::AlwaysFalse => {} - Truthiness::Ambiguous => { - return error( + } + + Type::ModuleLiteral(module) => { + let sym = if module + .module(db) + .known(db) + .is_some_and(KnownModule::is_builtins) + { + builtins_symbol(db, attribute) + } else { + module.static_member(db, attribute) + }; + if let Place::Defined(DefinedPlace { ty: attr_ty, .. }) = sym.place { + let value_ty = infer_value_ty(self, TypeContext::new(Some(attr_ty))); + + let assignable = value_ty.is_assignable_to(db, attr_ty); + if assignable { + true + } else { + if emit_diagnostics { + report_invalid_attribute_assignment( &self.context, - "The `contravariant` parameter of `TypeVar` \ - cannot have an ambiguous truthiness", - &kwarg.value, + target.into(), + attr_ty, + value_ty, + attribute, ); } + false } - } - "default" => { - if !have_features_from(PythonVersion::PY313) { - // We don't return here; this error is informational since this will error - // at runtime, but the user's intent is plain, we may as well respect it. - error( - &self.context, - "The `default` parameter of `typing.TypeVar` was added in Python 3.13", - kwarg, - ); - } + } else { + infer_value_ty(self, TypeContext::default()); - default = Some(TypeVarDefaultEvaluation::Lazy); - } - "infer_variance" => { - if !have_features_from(PythonVersion::PY312) { - // We don't return here; this error is informational since this will error - // at runtime, but the user's intent is plain, we may as well respect it. - error( - &self.context, - "The `infer_variance` parameter of `typing.TypeVar` was added in Python 3.12", - kwarg, - ); - } - // TODO support `infer_variance` in legacy TypeVars - if self - .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) - .is_ambiguous() + if emit_diagnostics + && let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) { - return error( - &self.context, - "The `infer_variance` parameter of `TypeVar` \ - cannot have an ambiguous truthiness", - &kwarg.value, - ); + builder.into_diagnostic(format_args!( + "Unresolved attribute `{}` on type `{}`.", + attribute, + object_ty.display(db) + )); } - } - name => { - // We don't return here; this error is informational since this will error - // at runtime, but it will likely cause fewer cascading errors if we just - // ignore the unknown keyword and still understand as much of the typevar as we - // can. - error( - &self.context, - format_args!("Unknown keyword argument `{name}` in `TypeVar` creation",), - kwarg, - ); - self.infer_expression(&kwarg.value, TypeContext::default()); - } - } - } - - let variance = match (covariant, contravariant) { - (true, true) => { - return error( - &self.context, - "A `TypeVar` cannot be both covariant and contravariant", - call_expr, - ); - } - (true, false) => TypeVarVariance::Covariant, - (false, true) => TypeVarVariance::Contravariant, - (false, false) => TypeVarVariance::Invariant, - }; - - let Some(name_param_ty) = name_param_ty.or_else(|| { - arguments - .find_positional(0) - .map(|arg| self.infer_expression(arg, TypeContext::default())) - }) else { - return error( - &self.context, - "The `name` parameter of `TypeVar` is required.", - call_expr, - ); - }; - - let Some(name_param) = name_param_ty.as_string_literal().map(|name| name.value(db)) else { - return error( - &self.context, - "The first argument to `TypeVar` must be a string literal.", - call_expr, - ); - }; - - let ast::Expr::Name(ast::ExprName { - id: target_name, .. - }) = target - else { - return error( - &self.context, - "A `TypeVar` definition must be a simple variable assignment", - target, - ); - }; - - if name_param != target_name { - return error( - &self.context, - format_args!( - "The name of a `TypeVar` (`{name_param}`) must match \ - the name of the variable it is assigned to (`{target_name}`)" - ), - target, - ); - } - // Inference of bounds, constraints, and defaults must be deferred, to avoid cycles. So we - // only check presence/absence/number here. - - let num_constraints = arguments.args.len().saturating_sub(1); - - let bound_or_constraints = match (has_bound, num_constraints) { - (false, 0) => None, - (true, 0) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), - (true, _) => { - return error( - &self.context, - "A `TypeVar` cannot have both a bound and constraints", - call_expr, - ); - } - (_, 1) => { - return error( - &self.context, - "A `TypeVar` cannot have exactly one constraint", - &arguments.args[1], - ); + false + } } - (false, _) => Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints), - }; - - if bound_or_constraints.is_some() || default.is_some() { - self.deferred.insert(definition, self.multi_inference_state); } - - let identity = TypeVarIdentity::new(db, target_name, Some(definition), TypeVarKind::Legacy); - Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( - db, - identity, - bound_or_constraints, - Some(variance), - default, - ))) } - fn infer_newtype_expression( + #[expect(clippy::type_complexity)] + fn infer_target_impl( &mut self, target: &ast::Expr, - call_expr: &ast::ExprCall, - definition: Definition<'db>, - ) -> Type<'db> { - fn error<'db>( - context: &InferContext<'db, '_>, - message: impl std::fmt::Display, - node: impl Ranged, - ) -> Type<'db> { - if let Some(builder) = context.report_lint(&INVALID_NEWTYPE, node) { - builder.into_diagnostic(message); - } - Type::unknown() - } + value: &ast::Expr, + infer_assigned_ty: Option<&dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>>, + ) { + match target { + ast::Expr::Name(name) => { + if let Some(infer_assigned_ty) = infer_assigned_ty { + infer_assigned_ty(self, TypeContext::default()); + } - let db = self.db(); - let arguments = &call_expr.arguments; + self.infer_definition(name); + } + ast::Expr::Starred(ast::ExprStarred { + value: starred_value, + .. + }) => { + self.infer_target_impl(starred_value, value, infer_assigned_ty); + } + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { + let assigned_ty = infer_assigned_ty.map(|f| f(self, TypeContext::default())); - if !arguments.keywords.is_empty() { - return error( - &self.context, - "Keyword arguments are not supported in `NewType` creation", - call_expr, - ); - } + if let Some(tuple_spec) = + assigned_ty.and_then(|ty| ty.tuple_instance_spec(self.db())) + { + let assigned_tys = tuple_spec.all_elements().to_vec(); - if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { - return error( - &self.context, - "Starred arguments are not supported in `NewType` creation", - starred, - ); - } + for (i, element) in elts.iter().enumerate() { + match assigned_tys.get(i).copied() { + None => self.infer_target_impl(element, value, None), + Some(ty) => self.infer_target_impl(element, value, Some(&|_, _| ty)), + } + } + } else { + for element in elts { + self.infer_target_impl(element, value, None); + } + } + } + ast::Expr::Attribute( + attr_expr @ ast::ExprAttribute { + value: object, + ctx: ExprContext::Store, + attr, + .. + }, + ) => { + let object_ty = self.infer_expression(object, TypeContext::default()); - if arguments.args.len() != 2 { - return error( - &self.context, - format!( - "Wrong number of arguments in `NewType` creation: expected 2, found {}", - arguments.args.len() - ), - call_expr, - ); - } + if let Some(infer_assigned_ty) = infer_assigned_ty { + let infer_assigned_ty = &mut |builder: &mut Self, tcx| { + let assigned_ty = infer_assigned_ty(builder, tcx); + builder.store_expression_type(target, assigned_ty); + assigned_ty + }; - let name_param_ty = self.infer_expression(&arguments.args[0], TypeContext::default()); + self.validate_attribute_assignment( + attr_expr, + object_ty, + attr.id(), + infer_assigned_ty, + true, + ); + } + } + ast::Expr::Subscript(subscript_expr) => { + if let Some(infer_assigned_ty) = infer_assigned_ty { + let infer_assigned_ty = &mut |builder: &mut Self, tcx| { + let assigned_ty = infer_assigned_ty(builder, tcx); + builder.store_expression_type(target, assigned_ty); + assigned_ty + }; - let Some(name) = name_param_ty.as_string_literal().map(|name| name.value(db)) else { - return error( - &self.context, - "The first argument to `NewType` must be a string literal", - call_expr, - ); - }; + self.validate_subscript_assignment(subscript_expr, value, infer_assigned_ty); + } + } - let ast::Expr::Name(ast::ExprName { - id: target_name, .. - }) = target - else { - return error( - &self.context, - "A `NewType` definition must be a simple variable assignment", - target, - ); - }; + // TODO: Remove this once we handle all possible assignment targets. + _ => { + if let Some(infer_assigned_ty) = infer_assigned_ty { + infer_assigned_ty(self, TypeContext::default()); + } - if name != target_name { - return error( - &self.context, - format_args!( - "The name of a `NewType` (`{name}`) must match \ - the name of the variable it is assigned to (`{target_name}`)" - ), - target, - ); + self.infer_expression(target, TypeContext::default()); + } } + } - // Inference of `tp` must be deferred, to avoid cycles. - self.deferred.insert(definition, self.multi_inference_state); + fn infer_assignment_definition( + &mut self, + assignment: &AssignmentDefinitionKind<'db>, + definition: Definition<'db>, + ) { + let target = assignment.target(self.module()); - Type::KnownInstance(KnownInstanceType::NewType(NewType::new( - db, - ast::name::Name::from(name), - definition, - None, - ))) + let add = self.add_binding(target.into(), definition); + let target_ty = + self.infer_assignment_definition_impl(assignment, definition, add.type_context()); + self.store_expression_type(target, target_ty); + add.insert(self, target_ty); } - fn infer_assignment_deferred(&mut self, target: &ast::Expr, value: &'ast ast::Expr) { - // Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec / NewType. - let ast::Expr::Call(ast::ExprCall { - func, arguments, .. - }) = value - else { - return; - }; - let func_ty = self - .try_expression_type(func) - .unwrap_or_else(|| self.infer_expression(func, TypeContext::default())); - if func_ty == Type::SpecialForm(SpecialFormType::NamedTuple) { - // Only the `fields` argument is deferred for `NamedTuple`; - // other arguments are inferred eagerly. - self.infer_typing_namedtuple_fields(&arguments.args[1]); - return; - } - let known_class = func_ty - .as_class_literal() - .and_then(|cls| cls.known(self.db())); - match (known_class, self.region) { - (Some(KnownClass::NewType), _) => { - self.infer_newtype_assignment_deferred(arguments); - return; - } - (Some(KnownClass::TypeAliasType), InferenceRegion::Deferred(definition)) => { - self.infer_typealiastype_assignment_deferred(definition, arguments); - return; - } - (Some(KnownClass::Type), InferenceRegion::Deferred(definition)) => { - self.infer_builtins_type_deferred(definition, value); - return; - } - _ => {} - } - let mut constraint_tys = Vec::new(); - for arg in arguments.args.iter().skip(1) { - let constraint = self.infer_type_expression(arg); - constraint_tys.push(constraint); + fn infer_assignment_definition_impl( + &mut self, + assignment: &AssignmentDefinitionKind<'db>, + definition: Definition<'db>, + tcx: TypeContext<'db>, + ) -> Type<'db> { + let value = assignment.value(self.module()); + let target = assignment.target(self.module()); - if constraint.has_typevar_or_typevar_instance(self.db()) - && let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, arg) - { - builder.into_diagnostic("TypeVar constraint cannot be generic"); - } - } - let mut bound_or_constraints = if !constraint_tys.is_empty() { - Some(TypeVarBoundOrConstraints::Constraints( - TypeVarConstraints::new(self.db(), constraint_tys.into_boxed_slice()), - )) - } else { - None - }; - if let Some(bound) = arguments.find_keyword("bound") { - let bound_type = self.infer_type_expression(&bound.value); - bound_or_constraints = Some(TypeVarBoundOrConstraints::UpperBound(bound_type)); + let mut target_ty = match assignment.target_kind() { + TargetKind::Sequence(unpack_position, unpack) => { + let unpacked = infer_unpack_types(self.db(), unpack); + // Only copy the diagnostics if this is the first assignment to avoid duplicating the + // unpack assignments. + if unpack_position == UnpackPosition::First { + self.context.extend(unpacked.diagnostics()); + } - if bound_type.has_typevar_or_typevar_instance(self.db()) - && let Some(builder) = self - .context - .report_lint(&INVALID_TYPE_VARIABLE_BOUND, bound) - { - builder.into_diagnostic("TypeVar upper bound cannot be generic"); - } - } - if let Some(default) = arguments.find_keyword("default") { - if matches!( - known_class, - Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) - ) { - // Pass `None` for the name: the outer-scope typevar check inside - // `infer_paramspec_default` is only relevant for PEP 695 type parameter - // scopes. Legacy ParamSpec definitions live at module/class-body scope, - // so the check would be a no-op here. Out-of-scope defaults for legacy - // typevars are instead validated by `check_legacy_typevar_defaults` - // (for functions) and `report_invalid_typevar_default_reference` - // (for classes). - self.infer_paramspec_default(&default.value, None); - } else { - let default_ty = self.infer_type_expression(&default.value); - let bound_or_constraints_node = arguments - .find_keyword("bound") - .map(|kw| BoundOrConstraintsNodes::Bound(&kw.value)) - .or_else(|| { - if arguments.args.len() < 3 { - return None; - } - Some(BoundOrConstraintsNodes::Constraints(&arguments.args[1..])) - }); - self.validate_typevar_default( - target.as_name_expr().map(|name| &*name.id), - bound_or_constraints, - default_ty, - &default.value, - bound_or_constraints_node, - ); + unpacked.expression_type(target) } - } - } + TargetKind::Single => { + // This could be an implicit type alias (OptionalList = list[T] | None). Use the definition + // of `OptionalList` as the binding context while inferring the RHS (`list[T] | None`), in + // order to bind `T` to `OptionalList`. + let previous_typevar_binding_context = + self.typevar_binding_context.replace(definition); + + let value_ty = if let Some(standalone_expression) = self.index.try_expression(value) + { + self.infer_standalone_expression_impl(value, standalone_expression, tcx) + } else if let ast::Expr::Call(call_expr) = value { + // If the RHS is not a standalone expression, this is a simple assignment + // (single target, no unpackings). That means it's a valid syntactic form + // for a legacy TypeVar creation; check for that. + let callable_type = self.infer_maybe_standalone_expression( + call_expr.func.as_ref(), + TypeContext::default(), + ); + + let ty = if let Some(namedtuple_kind) = + NamedTupleKind::from_type(self.db(), callable_type) + { + self.infer_namedtuple_call_expression( + call_expr, + Some(definition), + namedtuple_kind, + ) + } else { + match callable_type + .as_class_literal() + .and_then(|cls| cls.known(self.db())) + { + Some( + typevar_class @ (KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar), + ) => self.infer_legacy_typevar( + target, + call_expr, + definition, + typevar_class, + ), + Some( + paramspec_class @ (KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec), + ) => self.infer_legacy_paramspec( + target, + call_expr, + definition, + paramspec_class, + ), + Some(KnownClass::NewType) => { + self.infer_newtype_expression(target, call_expr, definition) + } + Some(KnownClass::Type) => { + // Try to extract the dynamic class with definition. + // This returns `None` if it's not a three-arg call to `type()`, + // signalling that we must fall back to normal call inference. + self.infer_builtins_type_call(call_expr, Some(definition)) + } + Some(KnownClass::TypeAliasType) => { + self.infer_typealiastype_call(target, call_expr, definition) + } + Some(_) | None => { + self.infer_call_expression_impl(call_expr, callable_type, tcx) + } + } + }; - // Infer the deferred base type of a NewType. - fn infer_newtype_assignment_deferred(&mut self, arguments: &ast::Arguments) { - let inferred = self.infer_type_expression(&arguments.args[1]); + self.store_expression_type(value, ty); + ty + } else { + self.infer_expression(value, tcx) + }; - if inferred.has_typevar_or_typevar_instance(self.db()) { - if let Some(builder) = self - .context - .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) - { - let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); - diag.set_primary_message("A `NewType` base cannot be generic"); - } - return; - } + self.typevar_binding_context = previous_typevar_binding_context; - match inferred { - Type::NewTypeInstance(_) | Type::NominalInstance(_) => return, - // There are exactly two union types allowed as bases for NewType: `int | float` and - // `int | float | complex`. These are allowed because that's what `float` and `complex` - // expand into in type position. We don't currently ask whether the union was implicit - // or explicit, so the explicit version is also allowed. - Type::Union(union_ty) => { - if let Some(KnownUnion::Float | KnownUnion::Complex) = union_ty.known(self.db()) { - return; + // `TYPE_CHECKING` is a special variable that should only be assigned `False` + // at runtime, but is always considered `True` in type checking. + // See mdtest/known_constants.md#user-defined-type_checking for details. + if target.as_name_expr().map(|name| name.id.as_str()) == Some("TYPE_CHECKING") { + if !matches!( + value.as_boolean_literal_expr(), + Some(ast::ExprBooleanLiteral { value: false, .. }) + ) { + report_invalid_type_checking_constant(&self.context, target.into()); + } + Type::bool_literal(true) + } else if self.in_stub() && value.is_ellipsis_literal_expr() { + Type::unknown() + } else { + value_ty } } - // `Unknown` is likely to be the result of an unresolved import or a typo, which will - // already get a diagnostic, so don't pile on an extra diagnostic here. - Type::Dynamic(DynamicType::Unknown) => return, - _ => {} - } - if let Some(builder) = self - .context - .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) - { - let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); - diag.set_primary_message(format!("type `{}`", inferred.display(self.db()))); - if matches!(inferred, Type::ProtocolInstance(_)) { - diag.info("The base of a `NewType` is not allowed to be a protocol class."); - } else if matches!(inferred, Type::TypedDict(_)) { - diag.info("The base of a `NewType` is not allowed to be a `TypedDict`."); - } else { - diag.info("The base of a `NewType` must be a class type or another `NewType`."); - } + }; + + if let Some(special_form) = target.as_name_expr().and_then(|name| { + SpecialFormType::try_from_file_and_name(self.db(), self.file(), &name.id) + }) { + target_ty = Type::SpecialForm(special_form); } + + target_ty } - /// Infer a `TypeAliasType("Name", value)` call in a simple assignment context. - /// - /// Follows the same pattern as [`Self::infer_newtype_expression`]: validates the - /// arguments, constructs a [`ManualPEP695TypeAliasType`], and defers inference of - /// the value argument. - fn infer_typealiastype_call( + fn infer_newtype_expression( &mut self, target: &ast::Expr, call_expr: &ast::ExprCall, @@ -6060,7 +2932,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { message: impl std::fmt::Display, node: impl Ranged, ) -> Type<'db> { - if let Some(builder) = context.report_lint(&INVALID_TYPE_ALIAS_TYPE, node) { + if let Some(builder) = context.report_lint(&INVALID_NEWTYPE, node) { builder.into_diagnostic(message); } Type::unknown() @@ -6069,10 +2941,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let arguments = &call_expr.arguments; + if !arguments.keywords.is_empty() { + return error( + &self.context, + "Keyword arguments are not supported in `NewType` creation", + call_expr, + ); + } + if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { return error( &self.context, - "Starred arguments are not supported in `TypeAliasType` creation", + "Starred arguments are not supported in `NewType` creation", starred, ); } @@ -6080,8 +2960,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if arguments.args.len() != 2 { return error( &self.context, - format_args!( - "Wrong number of arguments in `TypeAliasType` creation: expected 2, found {}", + format!( + "Wrong number of arguments in `NewType` creation: expected 2, found {}", arguments.args.len() ), call_expr, @@ -6093,8 +2973,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(name) = name_param_ty.as_string_literal().map(|name| name.value(db)) else { return error( &self.context, - "The first argument to `TypeAliasType` must be a string literal", - &arguments.args[0], + "The first argument to `NewType` must be a string literal", + call_expr, ); }; @@ -6104,1103 +2984,618 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return error( &self.context, - "A `TypeAliasType` definition must be a simple variable assignment", - target, - ); - }; - - if name != target_name { - return error( - &self.context, - format_args!( - "The name of a `TypeAliasType` (`{name}`) must match \ - the name of the variable it is assigned to (`{target_name}`)" - ), - target, - ); - } - - // Inference of the value argument must be deferred, to avoid cycles. - self.deferred.insert(definition, self.multi_inference_state); - - Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( - db, - ast::name::Name::new(name), - definition, - )), - )) - } - - /// Infer the deferred value type of a `TypeAliasType`. - fn infer_typealiastype_assignment_deferred( - &mut self, - definition: Definition<'db>, - arguments: &ast::Arguments, - ) { - // Match the binding context used by eager assignment inference so legacy type variables - // in the alias value are bound to the alias definition. - let previous_context = self.typevar_binding_context.replace(definition); - - self.infer_type_expression(&arguments.args[1]); - // Infer keyword arguments (e.g. `type_params`) so their types are stored. - for keyword in &arguments.keywords { - self.infer_expression(&keyword.value, TypeContext::default()); - } - - self.typevar_binding_context = previous_context; - } - - /// Deferred inference for assigned `type()` calls. - /// - /// Infers the bases argument that was skipped during initial inference to handle - /// forward references and recursive definitions. - fn infer_builtins_type_deferred(&mut self, definition: Definition<'db>, call_expr: &ast::Expr) { - let db = self.db(); - - let ast::Expr::Call(call) = call_expr else { - return; - }; - - // Get the already-inferred class type from the initial pass. - let inferred_type = definition_expression_type(db, definition, call_expr); - let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { - return; - }; - - let [_name_arg, bases_arg, _namespace_arg] = &*call.arguments.args else { - return; - }; - - // Set the typevar binding context to allow legacy typevar binding in expressions - // like `Generic[T]`. This matches the context used during initial inference. - let previous_context = self.typevar_binding_context.replace(definition); - - // Infer the bases argument (this was skipped during initial inference). - let bases_type = self.infer_expression(bases_arg, TypeContext::default()); - - // Restore the previous context. - self.typevar_binding_context = previous_context; - - // Extract and validate bases. - let Some(bases) = self.extract_explicit_bases(bases_arg, bases_type) else { - return; - }; - - // Validate individual bases for special types that aren't allowed in dynamic classes. - let name = dynamic_class.name(db); - self.validate_dynamic_type_bases(bases_arg, &bases, name); - } - - /// Infer a call to `builtins.type()`. - /// - /// `builtins.type` has two overloads: a single-argument overload (e.g. `type("foo")`, - /// and a 3-argument `type(name, bases, dict)` overload. Both are handled here. - /// The `definition` parameter should be `Some()` if this call to `builtins.type()` - /// occurs on the right-hand side of an assignment statement that has a [`Definition`] - /// associated with it in the semantic index. - /// - /// If it's unclear which overload we should pick, we return `type[Unknown]`, - /// to avoid cascading errors later on. - fn infer_builtins_type_call( - &mut self, - call_expr: &ast::ExprCall, - definition: Option>, - ) -> Type<'db> { - let db = self.db(); - - let ast::Arguments { - args, - keywords, - range: _, - node_index: _, - } = &call_expr.arguments; - - for keyword in keywords { - self.infer_expression(&keyword.value, TypeContext::default()); - } - - let [name_arg, bases_arg, namespace_arg] = match &**args { - [single] => { - let arg_type = self.infer_expression(single, TypeContext::default()); - - return if keywords.is_empty() { - arg_type.dunder_class(db) - } else { - if keywords.iter().any(|keyword| keyword.arg.is_some()) - && let Some(builder) = - self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) - { - let mut diagnostic = builder - .into_diagnostic("No overload of class `type` matches arguments"); - diagnostic.help(format_args!( - "`builtins.type()` expects no keyword arguments", - )); - } - SubclassOfType::subclass_of_unknown() - }; - } - - [first, second] if second.is_starred_expr() => { - self.infer_expression(first, TypeContext::default()); - self.infer_expression(second, TypeContext::default()); - - match &**keywords { - [single] if single.arg.is_none() => { - return SubclassOfType::subclass_of_unknown(); - } - _ => { - if let Some(builder) = - self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) - { - let mut diagnostic = builder - .into_diagnostic("No overload of class `type` matches arguments"); - diagnostic.help(format_args!( - "`builtins.type()` expects no keyword arguments", - )); - } + "A `NewType` definition must be a simple variable assignment", + target, + ); + }; - return SubclassOfType::subclass_of_unknown(); - } - } - } + if name != target_name { + return error( + &self.context, + format_args!( + "The name of a `NewType` (`{name}`) must match \ + the name of the variable it is assigned to (`{target_name}`)" + ), + target, + ); + } - [name, bases, namespace] => [name, bases, namespace], + // Inference of `tp` must be deferred, to avoid cycles. + self.deferred.insert(definition, self.multi_inference_state); - _ => { - for arg in args { - self.infer_expression(arg, TypeContext::default()); - } + Type::KnownInstance(KnownInstanceType::NewType(NewType::new( + db, + ast::name::Name::from(name), + definition, + None, + ))) + } - if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { - let mut diagnostic = - builder.into_diagnostic("No overload of class `type` matches arguments"); - diagnostic.help(format_args!( - "`builtins.type()` can either be called with one or three \ - positional arguments (got {})", - args.len() - )); - } + fn infer_assignment_deferred(&mut self, target: &ast::Expr, value: &'ast ast::Expr) { + // Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec / NewType. + let ast::Expr::Call(ast::ExprCall { + func, arguments, .. + }) = value + else { + return; + }; + let func_ty = self + .try_expression_type(func) + .unwrap_or_else(|| self.infer_expression(func, TypeContext::default())); + if func_ty == Type::SpecialForm(SpecialFormType::NamedTuple) { + // Only the `fields` argument is deferred for `NamedTuple`; + // other arguments are inferred eagerly. + self.infer_typing_namedtuple_fields(&arguments.args[1]); + return; + } + let known_class = func_ty + .as_class_literal() + .and_then(|cls| cls.known(self.db())); + match (known_class, self.region) { + (Some(KnownClass::NewType), _) => { + self.infer_newtype_assignment_deferred(arguments); + return; + } + (Some(KnownClass::TypeAliasType), InferenceRegion::Deferred(definition)) => { + self.infer_typealiastype_assignment_deferred(definition, arguments); + return; + } + (Some(KnownClass::Type), InferenceRegion::Deferred(definition)) => { + self.infer_builtins_type_deferred(definition, value); + return; + } + _ => {} + } + let mut constraint_tys = Vec::new(); + for arg in arguments.args.iter().skip(1) { + let constraint = self.infer_type_expression(arg); + constraint_tys.push(constraint); - return SubclassOfType::subclass_of_unknown(); + if constraint.has_typevar_or_typevar_instance(self.db()) + && let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, arg) + { + builder.into_diagnostic("TypeVar constraint cannot be generic"); } + } + let mut bound_or_constraints = if !constraint_tys.is_empty() { + Some(TypeVarBoundOrConstraints::Constraints( + TypeVarConstraints::new(self.db(), constraint_tys.into_boxed_slice()), + )) + } else { + None }; + if let Some(bound) = arguments.find_keyword("bound") { + let bound_type = self.infer_type_expression(&bound.value); + bound_or_constraints = Some(TypeVarBoundOrConstraints::UpperBound(bound_type)); - let name_type = self.infer_expression(name_arg, TypeContext::default()); + if bound_type.has_typevar_or_typevar_instance(self.db()) + && let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_BOUND, bound) + { + builder.into_diagnostic("TypeVar upper bound cannot be generic"); + } + } + if let Some(default) = arguments.find_keyword("default") { + if matches!( + known_class, + Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) + ) { + // Pass `None` for the name: the outer-scope typevar check inside + // `infer_paramspec_default` is only relevant for PEP 695 type parameter + // scopes. Legacy ParamSpec definitions live at module/class-body scope, + // so the check would be a no-op here. Out-of-scope defaults for legacy + // typevars are instead validated by `check_legacy_typevar_defaults` + // (for functions) and `report_invalid_typevar_default_reference` + // (for classes). + self.infer_paramspec_default(&default.value, None); + } else { + let default_ty = self.infer_type_expression(&default.value); + let bound_or_constraints_node = arguments + .find_keyword("bound") + .map(|kw| BoundOrConstraintsNodes::Bound(&kw.value)) + .or_else(|| { + if arguments.args.len() < 3 { + return None; + } + Some(BoundOrConstraintsNodes::Constraints(&arguments.args[1..])) + }); + self.validate_typevar_default( + target.as_name_expr().map(|name| &*name.id), + bound_or_constraints, + default_ty, + &default.value, + bound_or_constraints_node, + ); + } + } + } - let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); + // Infer the deferred base type of a NewType. + fn infer_newtype_assignment_deferred(&mut self, arguments: &ast::Arguments) { + let inferred = self.infer_type_expression(&arguments.args[1]); - // TODO: validate other keywords against `__init_subclass__` methods of superclasses - if keywords - .iter() - .filter_map(|keyword| keyword.arg.as_deref()) - .contains("metaclass") - { - if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { - let mut diagnostic = - builder.into_diagnostic("No overload of class `type` matches arguments"); - diagnostic - .help("The `metaclass` keyword argument is not supported in `type()` calls"); + if inferred.has_typevar_or_typevar_instance(self.db()) { + if let Some(builder) = self + .context + .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) + { + let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); + diag.set_primary_message("A `NewType` base cannot be generic"); } + return; } - // If any argument is a starred expression, we can't know how many positional arguments - // we're receiving, so fall back to `type[Unknown]` to avoid false-positive errors. - if args.iter().any(ast::Expr::is_starred_expr) { - return SubclassOfType::subclass_of_unknown(); + match inferred { + Type::NewTypeInstance(_) | Type::NominalInstance(_) => return, + // There are exactly two union types allowed as bases for NewType: `int | float` and + // `int | float | complex`. These are allowed because that's what `float` and `complex` + // expand into in type position. We don't currently ask whether the union was implicit + // or explicit, so the explicit version is also allowed. + Type::Union(union_ty) => { + if let Some(KnownUnion::Float | KnownUnion::Complex) = union_ty.known(self.db()) { + return; + } + } + // `Unknown` is likely to be the result of an unresolved import or a typo, which will + // already get a diagnostic, so don't pile on an extra diagnostic here. + Type::Dynamic(DynamicType::Unknown) => return, + _ => {} } - - // Extract members from the namespace dict (third argument). - let (members, has_dynamic_namespace): (Box<[(ast::name::Name, Type<'db>)]>, bool) = - if let ast::Expr::Dict(dict) = namespace_arg { - // Check if all keys are string literal types. If any key is not a string literal - // type or is missing (spread), the namespace is considered dynamic. - let all_keys_are_string_literals = dict.items.iter().all(|item| { - item.key - .as_ref() - .is_some_and(|k| self.expression_type(k).is_string_literal()) - }); - let members = dict - .items - .iter() - .filter_map(|item| { - // Only extract items with string literal keys. - let key_expr = item.key.as_ref()?; - let key_name = self.expression_type(key_expr).as_string_literal()?; - let key_name = ast::name::Name::new(key_name.value(db)); - // Get the already-inferred type from when we inferred the dict above. - let value_ty = self.expression_type(&item.value); - Some((key_name, value_ty)) - }) - .collect(); - (members, !all_keys_are_string_literals) - } else if let Type::TypedDict(typed_dict) = namespace_type { - // `namespace` is a TypedDict instance. Extract known keys as members. - // TypedDicts are "open" (can have additional string keys), so this - // is still a dynamic namespace for unknown attributes. - let members: Box<[(ast::name::Name, Type<'db>)]> = typed_dict - .items(db) - .iter() - .map(|(name, field)| (name.clone(), field.declared_ty)) - .collect(); - (members, true) + if let Some(builder) = self + .context + .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) + { + let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); + diag.set_primary_message(format!("type `{}`", inferred.display(self.db()))); + if matches!(inferred, Type::ProtocolInstance(_)) { + diag.info("The base of a `NewType` is not allowed to be a protocol class."); + } else if matches!(inferred, Type::TypedDict(_)) { + diag.info("The base of a `NewType` is not allowed to be a `TypedDict`."); } else { - // `namespace` is not a dict literal, so it's dynamic. - (Box::new([]), true) - }; + diag.info("The base of a `NewType` must be a class type or another `NewType`."); + } + } + } - if !matches!(namespace_type, Type::TypedDict(_)) - && !namespace_type.is_assignable_to( - db, - KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]), - ) - && let Some(builder) = self - .context - .report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg) - { - let mut diagnostic = builder - .into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`"); - diagnostic.set_primary_message(format_args!( - "Expected `dict[str, Any]`, found `{}`", - namespace_type.display(db) - )); + /// Infer a `TypeAliasType("Name", value)` call in a simple assignment context. + /// + /// Follows the same pattern as [`Self::infer_newtype_expression`]: validates the + /// arguments, constructs a [`ManualPEP695TypeAliasType`], and defers inference of + /// the value argument. + fn infer_typealiastype_call( + &mut self, + target: &ast::Expr, + call_expr: &ast::ExprCall, + definition: Definition<'db>, + ) -> Type<'db> { + fn error<'db>( + context: &InferContext<'db, '_>, + message: impl std::fmt::Display, + node: impl Ranged, + ) -> Type<'db> { + if let Some(builder) = context.report_lint(&INVALID_TYPE_ALIAS_TYPE, node) { + builder.into_diagnostic(message); + } + Type::unknown() } - // Extract name and base classes. - let name = if let Some(literal) = name_type.as_string_literal() { - Name::new(literal.value(db)) - } else { - if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) - && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) - { - let mut diagnostic = - builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`"); - diagnostic.set_primary_message(format_args!( - "Expected `str`, found `{}`", - name_type.display(db) - )); - } - Name::new_static("") - }; + let db = self.db(); + let arguments = &call_expr.arguments; - let scope = self.scope(); + if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { + return error( + &self.context, + "Starred arguments are not supported in `TypeAliasType` creation", + starred, + ); + } - // For assigned `type()` calls, bases inference is deferred to handle forward references - // and recursive references (e.g., `X = type("X", (tuple["X | None"],), {})`). - // This avoids expensive Salsa fixpoint iteration by deferring inference until the - // class type is already bound. For dangling calls, infer and extract bases eagerly - // (they'll be stored in the anchor and used for validation). - let explicit_bases = if definition.is_none() { - let bases_type = self.infer_expression(bases_arg, TypeContext::default()); - self.extract_explicit_bases(bases_arg, bases_type) - } else { - None - }; + if arguments.args.len() != 2 { + return error( + &self.context, + format_args!( + "Wrong number of arguments in `TypeAliasType` creation: expected 2, found {}", + arguments.args.len() + ), + call_expr, + ); + } - // Create the anchor for identifying this dynamic class. - // - For assigned `type()` calls, the Definition uniquely identifies the class, - // and bases inference is deferred. - // - For dangling calls, compute a relative offset from the scope's node index, - // and store the explicit bases directly (since they were inferred eagerly). - let anchor = if let Some(def) = definition { - // Register for deferred inference to infer bases and validate later. - self.deferred.insert(def, self.multi_inference_state); - DynamicClassAnchor::Definition(def) - } else { - let call_node_index = call_expr.node_index().load(); - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("scope anchor should not be NodeIndex::NONE"); - let call_u32 = call_node_index - .as_u32() - .expect("call node should not be NodeIndex::NONE"); + let name_param_ty = self.infer_expression(&arguments.args[0], TypeContext::default()); - // Use [Unknown] as fallback if bases extraction failed (e.g., not a tuple). - let anchor_bases = explicit_bases - .clone() - .unwrap_or_else(|| Box::from([Type::unknown()])); + let Some(name) = name_param_ty.as_string_literal().map(|name| name.value(db)) else { + return error( + &self.context, + "The first argument to `TypeAliasType` must be a string literal", + &arguments.args[0], + ); + }; - DynamicClassAnchor::ScopeOffset { - scope, - offset: call_u32 - anchor_u32, - explicit_bases: anchor_bases, - } + let ast::Expr::Name(ast::ExprName { + id: target_name, .. + }) = target + else { + return error( + &self.context, + "A `TypeAliasType` definition must be a simple variable assignment", + target, + ); }; - let dynamic_class = DynamicClassLiteral::new( - db, - name.clone(), - anchor, - members, - has_dynamic_namespace, - None, - ); + if name != target_name { + return error( + &self.context, + format_args!( + "The name of a `TypeAliasType` (`{name}`) must match \ + the name of the variable it is assigned to (`{target_name}`)" + ), + target, + ); + } - // For dangling calls, validate bases eagerly. For assigned calls, validation is - // deferred along with bases inference. - if let Some(explicit_bases) = &explicit_bases { - // Validate bases and collect disjoint bases for diagnostics. - let mut disjoint_bases = - self.validate_dynamic_type_bases(bases_arg, explicit_bases, &name); + // Inference of the value argument must be deferred, to avoid cycles. + self.deferred.insert(definition, self.multi_inference_state); - // Check for MRO errors. - if report_dynamic_mro_errors(&self.context, dynamic_class, call_expr, bases_arg) { - // MRO succeeded, check for instance-layout-conflict. - disjoint_bases.remove_redundant_entries(db); - if disjoint_bases.len() > 1 { - report_instance_layout_conflict( - &self.context, - dynamic_class.header_range(db), - bases_arg.as_tuple_expr().map(|tuple| tuple.elts.as_slice()), - &disjoint_bases, - ); - } - } + Type::KnownInstance(KnownInstanceType::TypeAliasType( + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, + ast::name::Name::new(name), + definition, + )), + )) + } - // Check for metaclass conflicts. - if let Err(DynamicMetaclassConflict { - metaclass1, - base1, - metaclass2, - base2, - }) = dynamic_class.try_metaclass(db) - { - report_conflicting_metaclass_from_bases( - &self.context, - call_expr.into(), - dynamic_class.name(db), - metaclass1, - base1.display(db), - metaclass2, - base2.display(db), - ); - } + /// Infer the deferred value type of a `TypeAliasType`. + fn infer_typealiastype_assignment_deferred( + &mut self, + definition: Definition<'db>, + arguments: &ast::Arguments, + ) { + // Match the binding context used by eager assignment inference so legacy type variables + // in the alias value are bound to the alias definition. + let previous_context = self.typevar_binding_context.replace(definition); + + self.infer_type_expression(&arguments.args[1]); + // Infer keyword arguments (e.g. `type_params`) so their types are stored. + for keyword in &arguments.keywords { + self.infer_expression(&keyword.value, TypeContext::default()); } - Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) + self.typevar_binding_context = previous_context; } - /// Infer a `typing.NamedTuple(typename, fields)` or `collections.namedtuple(typename, field_names)` call. + /// Deferred inference for assigned `type()` calls. /// - /// This method *does not* call `infer_expression` on the object being called; - /// it is assumed that the type for this AST node has already been inferred before this method is called. - fn infer_namedtuple_call_expression( - &mut self, - call_expr: &ast::ExprCall, - definition: Option>, - kind: NamedTupleKind, - ) -> Type<'db> { + /// Infers the bases argument that was skipped during initial inference to handle + /// forward references and recursive definitions. + fn infer_builtins_type_deferred(&mut self, definition: Definition<'db>, call_expr: &ast::Expr) { let db = self.db(); - // The fallback type reflects the fact that if the call were successful, - // it would return a class that: - // - // - Would be a subclass of `tuple[Unknown, ...]` - // - Would have all the generated methods included on the `NamedTupleLike` protocol - // - Would have a constructor method that would accept an unknown set of positional - // and keyword arguments - let fallback = || { - IntersectionType::from_elements( - db, - [ - Type::homogeneous_tuple(db, Type::unknown()).to_meta_type(db), - KnownClass::NamedTupleLike.to_subclass_of(db), - Type::unknown(), - ], - ) + let ast::Expr::Call(call) = call_expr else { + return; }; - let ast::Arguments { - args, - keywords, - range: _, - node_index: _, - } = &call_expr.arguments; + // Get the already-inferred class type from the initial pass. + let inferred_type = definition_expression_type(db, definition, call_expr); + let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { + return; + }; - // Check for variadic arguments early, before extracting positional args. - let has_starred = args.iter().any(ast::Expr::is_starred_expr); - let has_double_starred = keywords.iter().any(|kw| kw.arg.is_none()); + let [_name_arg, bases_arg, _namespace_arg] = &*call.arguments.args else { + return; + }; - // Emit diagnostic for missing required arguments or unsupported variadic arguments. - // For `typing.NamedTuple`, emit a diagnostic since variadic arguments are not supported. - // For `collections.namedtuple`, silently fall back since it's more permissive at runtime. - if (has_starred || has_double_starred) - && kind.is_typing() - && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, call_expr) - { - let arg_type = if has_starred && has_double_starred { - "Variadic positional and keyword arguments are" - } else if has_starred { - "Variadic positional arguments are" - } else { - "Variadic keyword arguments are" - }; - builder.into_diagnostic(format_args!( - "{arg_type} not supported in `NamedTuple()` calls" - )); - } + // Set the typevar binding context to allow legacy typevar binding in expressions + // like `Generic[T]`. This matches the context used during initial inference. + let previous_context = self.typevar_binding_context.replace(definition); - // Extract typename and fields from positional or keyword arguments. - // For `collections.namedtuple`, both `typename` and `field_names` can be keyword arguments. - // For `typing.NamedTuple`, only positional arguments are supported. - let (name_arg, fields_arg, rest, name_from_keyword, fields_from_keyword): ( - Option<&ast::Expr>, - Option<&ast::Expr>, - &[ast::Expr], - bool, - bool, - ) = match kind { - NamedTupleKind::Collections => { - let typename_kw = call_expr.arguments.find_keyword("typename"); - let field_names_kw = call_expr.arguments.find_keyword("field_names"); - - match &**args { - [name, fields, rest @ ..] => (Some(name), Some(fields), rest, false, false), - [name, rest @ ..] => ( - Some(name), - field_names_kw.map(|kw| &kw.value), - rest, - false, - field_names_kw.is_some(), - ), - [] => ( - typename_kw.map(|kw| &kw.value), - field_names_kw.map(|kw| &kw.value), - &[], - typename_kw.is_some(), - field_names_kw.is_some(), - ), - } - } - NamedTupleKind::Typing => match &**args { - [name, fields, rest @ ..] => (Some(name), Some(fields), rest, false, false), - [name, rest @ ..] => (Some(name), None, rest, false, false), - [] => (None, None, &[], false, false), - }, - }; + // Infer the bases argument (this was skipped during initial inference). + let bases_type = self.infer_expression(bases_arg, TypeContext::default()); - // Check if we have both required arguments. - let (Some(name_arg), Some(fields_arg)) = (name_arg, fields_arg) else { - for arg in args { - self.infer_expression(arg, TypeContext::default()); - } - for kw in keywords { - self.infer_expression(&kw.value, TypeContext::default()); - } + // Restore the previous context. + self.typevar_binding_context = previous_context; - if !has_starred && !has_double_starred { - let fields_param_name = match kind { - NamedTupleKind::Typing => "fields", - NamedTupleKind::Collections => "field_names", - }; - let missing = match (name_arg.is_none(), fields_arg.is_none()) { - (true, true) => format!("`typename` and `{fields_param_name}`"), - (true, false) => "`typename`".to_string(), - (false, true) => format!("`{fields_param_name}`"), - (false, false) => unreachable!(), - }; - let plural = name_arg.is_none() && fields_arg.is_none(); - if let Some(builder) = self.context.report_lint(&MISSING_ARGUMENT, call_expr) { - builder.into_diagnostic(format_args!( - "Missing required argument{} {missing} to `{kind}()`", - if plural { "s" } else { "" } - )); - } - } - return fallback(); + // Extract and validate bases. + let Some(bases) = self.extract_explicit_bases(bases_arg, bases_type) else { + return; }; - let name_type = self.infer_expression(name_arg, TypeContext::default()); + // Validate individual bases for special types that aren't allowed in dynamic classes. + let name = dynamic_class.name(db); + self.validate_dynamic_type_bases(bases_arg, &bases, name); + } - for arg in rest { - self.infer_expression(arg, TypeContext::default()); - } + /// Infer a call to `builtins.type()`. + /// + /// `builtins.type` has two overloads: a single-argument overload (e.g. `type("foo")`, + /// and a 3-argument `type(name, bases, dict)` overload. Both are handled here. + /// The `definition` parameter should be `Some()` if this call to `builtins.type()` + /// occurs on the right-hand side of an assignment statement that has a [`Definition`] + /// associated with it in the semantic index. + /// + /// If it's unclear which overload we should pick, we return `type[Unknown]`, + /// to avoid cascading errors later on. + fn infer_builtins_type_call( + &mut self, + call_expr: &ast::ExprCall, + definition: Option>, + ) -> Type<'db> { + let db = self.db(); - // If any argument is a starred expression or any keyword is a double-starred expression, - // we can't statically determine the arguments, so fall back to normal call binding. - if has_starred || has_double_starred { - for kw in keywords { - self.infer_expression(&kw.value, TypeContext::default()); - } - return fallback(); - } + let ast::Arguments { + args, + keywords, + range: _, + node_index: _, + } = &call_expr.arguments; - // Check for excess positional arguments (only `typename` and `fields` are expected). - if !rest.is_empty() { - if let Some(builder) = self - .context - .report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, &rest[0]) - { - builder.into_diagnostic(format_args!( - "Too many positional arguments to function `{kind}`: expected 2, got {}", - args.len() - )); - } + for keyword in keywords { + self.infer_expression(&keyword.value, TypeContext::default()); } - // Infer keyword arguments. - let mut default_types: Vec> = vec![]; - let mut defaults_kw: Option<&ast::Keyword> = None; - let mut rename_type = None; - - for kw in keywords { - // `kw.arg` is `None` for double-starred kwargs (`**kwargs`), but we already - // returned early above if there were any, so this should always be `Some`. - let arg = kw - .arg - .as_ref() - .expect("double-starred kwargs should have been handled above"); - - // Skip keywords that were used for the required arguments (already inferred above). - // These flags are only true for `collections.namedtuple`. - if name_from_keyword && arg.id.as_str() == "typename" { - continue; - } - if fields_from_keyword && arg.id.as_str() == "field_names" { - continue; - } + let [name_arg, bases_arg, namespace_arg] = match &**args { + [single] => { + let arg_type = self.infer_expression(single, TypeContext::default()); - let kw_type = self.infer_expression(&kw.value, TypeContext::default()); - - match arg.id.as_str() { - "defaults" if kind.is_collections() => { - defaults_kw = Some(kw); - // Extract element types from AST literals (using already-inferred types) - // or fall back to the inferred tuple spec. - match &kw.value { - ast::Expr::List(list) => { - // Elements were already inferred when we inferred kw.value above. - default_types = list - .elts - .iter() - .map(|elt| self.expression_type(elt)) - .collect(); - } - ast::Expr::Tuple(tuple) => { - // Elements were already inferred when we inferred kw.value above. - default_types = tuple - .elts - .iter() - .map(|elt| self.expression_type(elt)) - .collect(); - } - _ => { - // Fall back to using the already-inferred type. - // Try to extract element types from tuple. - if let Some(spec) = kw_type.exact_tuple_instance_spec(db) - && let Some(fixed) = spec.as_fixed_length() - { - default_types = fixed.all_elements().to_vec(); - } else { - // Can't determine individual types; use Any for each element. - let count = kw_type - .exact_tuple_instance_spec(db) - .and_then(|spec| spec.len().maximum()) - .unwrap_or(0); - default_types = vec![Type::any(); count]; - } - } - } - // Emit diagnostic for invalid types (not Iterable[Any] | None). - let iterable_any = - KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); - let valid_type = UnionType::from_two_elements(db, iterable_any, Type::none(db)); - if !kw_type.is_assignable_to(db, valid_type) + return if keywords.is_empty() { + arg_type.dunder_class(db) + } else { + if keywords.iter().any(|keyword| keyword.arg.is_some()) && let Some(builder) = - self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) + self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `defaults` of `namedtuple()`" - )); - diagnostic.set_primary_message(format_args!( - "Expected `Iterable[Any] | None`, found `{}`", - kw_type.display(db) + let mut diagnostic = builder + .into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` expects no keyword arguments", )); } - } - "rename" if kind.is_collections() => { - rename_type = Some(kw_type); + SubclassOfType::subclass_of_unknown() + }; + } - // Emit diagnostic for non-bool types. - if !kw_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) - && let Some(builder) = - self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `rename` of `namedtuple()`" - )); - diagnostic.set_primary_message(format_args!( - "Expected `bool`, found `{}`", - kw_type.display(db) - )); - } - } - "module" if kind.is_collections() => { - // Emit diagnostic for invalid types (not str | None). - let valid_type = UnionType::from_two_elements( - db, - KnownClass::Str.to_instance(db), - Type::none(db), - ); - if !kw_type.is_assignable_to(db, valid_type) - && let Some(builder) = - self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `module` of `namedtuple()`" - )); - diagnostic.set_primary_message(format_args!( - "Expected `str | None`, found `{}`", - kw_type.display(db) - )); - } - } - // `typename` is valid as a keyword argument only for `collections.namedtuple`. - // If it was already provided positionally, emit an error. - "typename" if kind.is_collections() => { - if !args.is_empty() { - if let Some(builder) = - self.context.report_lint(&PARAMETER_ALREADY_ASSIGNED, kw) - { - builder.into_diagnostic(format_args!( - "Multiple values provided for parameter `typename` of `{kind}`" - )); - } + [first, second] if second.is_starred_expr() => { + self.infer_expression(first, TypeContext::default()); + self.infer_expression(second, TypeContext::default()); + + match &**keywords { + [single] if single.arg.is_none() => { + return SubclassOfType::subclass_of_unknown(); } - } - // `field_names` is valid only for `collections.namedtuple`. - // If it was already provided positionally, emit an error. - "field_names" if kind.is_collections() => { - if args.len() >= 2 { + _ => { if let Some(builder) = - self.context.report_lint(&PARAMETER_ALREADY_ASSIGNED, kw) + self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { - builder.into_diagnostic(format_args!( - "Multiple values provided for parameter `field_names` of `{kind}`" + let mut diagnostic = builder + .into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` expects no keyword arguments", )); } - } - } - unknown_kwarg => { - // Report unknown keyword argument. - if let Some(builder) = self.context.report_lint(&UNKNOWN_ARGUMENT, kw) { - builder.into_diagnostic(format_args!( - "Argument `{unknown_kwarg}` does not match any known parameter of function `{kind}`", - )); + + return SubclassOfType::subclass_of_unknown(); } } } - } - // Extract name. - let name = if let Some(literal) = name_type.as_string_literal() { - Name::new(literal.value(db)) - } else { - // Name is not a string literal; use like we do for type() calls. - if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) - && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `typename` of `{kind}()`" - )); - diagnostic.set_primary_message(format_args!( - "Expected `str`, found `{}`", - name_type.display(db) - )); - } - Name::new_static("") - }; + [name, bases, namespace] => [name, bases, namespace], - // Handle fields based on which namedtuple variant. - let anchor = match definition { - Some(definition) => match kind { - NamedTupleKind::Collections => { - let spec = self.infer_collections_namedtuple_fields( - rename_type, - fields_arg, - &default_types, - defaults_kw, - ); - DynamicNamedTupleAnchor::CollectionsDefinition { definition, spec } - } - NamedTupleKind::Typing => { - // The `fields` argument to `typing.NamedTuple` cannot be inferred - // eagerly if it's not a dangling call, as it may contain forward references - // or recursive references. - self.deferred.insert(definition, self.multi_inference_state); - DynamicNamedTupleAnchor::TypingDefinition(definition) + _ => { + for arg in args { + self.infer_expression(arg, TypeContext::default()); } - }, - None => { - let call_node_index = call_expr.node_index.load(); - let scope = self.scope(); - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("scope anchor should not be NodeIndex::NONE"); - let call_u32 = call_node_index - .as_u32() - .expect("call node should not be NodeIndex::NONE"); - let spec = match kind { - NamedTupleKind::Collections => self.infer_collections_namedtuple_fields( - rename_type, - fields_arg, - &default_types, - defaults_kw, - ), - NamedTupleKind::Typing => self.infer_typing_namedtuple_fields(fields_arg), - }; - DynamicNamedTupleAnchor::ScopeOffset { - scope, - offset: call_u32 - anchor_u32, - spec, + + if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { + let mut diagnostic = + builder.into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` can either be called with one or three \ + positional arguments (got {})", + args.len() + )); } + + return SubclassOfType::subclass_of_unknown(); } }; - let namedtuple = DynamicNamedTupleLiteral::new(db, name, anchor); - - Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) - } - - fn infer_collections_namedtuple_fields( - &mut self, - rename_type: Option>, - fields_arg: &ast::Expr, - default_types: &[Type<'db>], - defaults_kw: Option<&ast::Keyword>, - ) -> NamedTupleSpec<'db> { - let db = self.db(); - - // `collections.namedtuple`: `field_names` is a list or tuple of strings, or a space or - // comma-separated string. - - // Check for `rename=True`. Use `is_always_true()` to handle truthy values - // (e.g., `rename=1`), though we'd still want a diagnostic for non-bool types. - let rename = rename_type.is_some_and(|ty| ty.bool(db).is_always_true()); - - let fields_type = self.infer_expression(fields_arg, TypeContext::default()); - - // Extract field names, first from the inferred type, then from the AST. - let maybe_field_names: Option> = - if let Some(string_literal) = fields_type.as_string_literal() { - // Handle space/comma-separated string. - Some( - string_literal - .value(db) - .replace(',', " ") - .split_whitespace() - .map(Name::new) - .collect(), - ) - } else if let Some(tuple_spec) = fields_type.tuple_instance_spec(db) - && let Some(fixed_tuple) = tuple_spec.as_fixed_length() - { - // Handle list/tuple of strings (must be fixed-length). - fixed_tuple - .all_elements() - .iter() - .map(|elt| elt.as_string_literal().map(|s| Name::new(s.value(db)))) - .collect() - } else { - // Get the elements from the list or tuple literal. - let elements = match fields_arg { - ast::Expr::List(list) => Some(&list.elts), - ast::Expr::Tuple(tuple) => Some(&tuple.elts), - _ => None, - }; + let name_type = self.infer_expression(name_arg, TypeContext::default()); - elements.and_then(|elts| { - elts.iter() - .map(|elt| { - // Each element should be a string literal. - let field_ty = self.expression_type(elt); - let field_lit = field_ty.as_string_literal()?; - Some(Name::new(field_lit.value(db))) - }) - .collect::>() - }) - }; + let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); - if maybe_field_names.is_none() { - // Emit diagnostic if the type is outright invalid (not str | Iterable[str]). - let iterable_str = KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); - let valid_type = - UnionType::from_two_elements(db, KnownClass::Str.to_instance(db), iterable_str); - if !fields_type.is_assignable_to(db, valid_type) - && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, fields_arg) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `field_names` of `namedtuple()`" - )); - diagnostic.set_primary_message(format_args!( - "Expected `str` or an iterable of strings, found `{}`", - fields_type.display(db) - )); + // TODO: validate other keywords against `__init_subclass__` methods of superclasses + if keywords + .iter() + .filter_map(|keyword| keyword.arg.as_deref()) + .contains("metaclass") + { + if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { + let mut diagnostic = + builder.into_diagnostic("No overload of class `type` matches arguments"); + diagnostic + .help("The `metaclass` keyword argument is not supported in `type()` calls"); } } - let Some(mut field_names) = maybe_field_names else { - // Couldn't determine fields statically; attribute lookups will return Any. - return NamedTupleSpec::unknown(db); - }; - - // When `rename` is false (or not specified), emit diagnostics for invalid - // field names. These all raise ValueError at runtime. When `rename=True`, - // invalid names are automatically replaced with `_0`, `_1`, etc., so no - // diagnostic is needed. - if !rename { - self.check_invalid_namedtuple_field_names( - &field_names, - fields_arg, - NamedTupleKind::Collections, - ); - } else { - // Apply rename logic. - let mut seen_names = FxHashSet::<&str>::default(); - for (i, field_name) in field_names.iter_mut().enumerate() { - let name_str = field_name.as_str(); - let needs_rename = name_str.starts_with('_') - || is_keyword(name_str) - || !is_identifier(name_str) - || seen_names.contains(name_str); - if needs_rename { - *field_name = Name::new(format!("_{i}")); - } - seen_names.insert(field_name.as_str()); - } + // If any argument is a starred expression, we can't know how many positional arguments + // we're receiving, so fall back to `type[Unknown]` to avoid false-positive errors. + if args.iter().any(ast::Expr::is_starred_expr) { + return SubclassOfType::subclass_of_unknown(); } - let num_fields = field_names.len(); - let defaults_count = default_types.len(); + // Extract members from the namespace dict (third argument). + let (members, has_dynamic_namespace): (Box<[(ast::name::Name, Type<'db>)]>, bool) = + if let ast::Expr::Dict(dict) = namespace_arg { + // Check if all keys are string literal types. If any key is not a string literal + // type or is missing (spread), the namespace is considered dynamic. + let all_keys_are_string_literals = dict.items.iter().all(|item| { + item.key + .as_ref() + .is_some_and(|k| self.expression_type(k).is_string_literal()) + }); + let members = dict + .items + .iter() + .filter_map(|item| { + // Only extract items with string literal keys. + let key_expr = item.key.as_ref()?; + let key_name = self.expression_type(key_expr).as_string_literal()?; + let key_name = ast::name::Name::new(key_name.value(db)); + // Get the already-inferred type from when we inferred the dict above. + let value_ty = self.expression_type(&item.value); + Some((key_name, value_ty)) + }) + .collect(); + (members, !all_keys_are_string_literals) + } else if let Type::TypedDict(typed_dict) = namespace_type { + // `namespace` is a TypedDict instance. Extract known keys as members. + // TypedDicts are "open" (can have additional string keys), so this + // is still a dynamic namespace for unknown attributes. + let members: Box<[(ast::name::Name, Type<'db>)]> = typed_dict + .items(db) + .iter() + .map(|(name, field)| (name.clone(), field.declared_ty)) + .collect(); + (members, true) + } else { + // `namespace` is not a dict literal, so it's dynamic. + (Box::new([]), true) + }; - if defaults_count > num_fields - && let Some(defaults_kw) = defaults_kw - && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, defaults_kw) + if !matches!(namespace_type, Type::TypedDict(_)) + && !namespace_type.is_assignable_to( + db, + KnownClass::Dict + .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]), + ) + && let Some(builder) = self + .context + .report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg) { - let mut diagnostic = - builder.into_diagnostic(format_args!("Too many defaults for `namedtuple()`")); + let mut diagnostic = builder + .into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`"); diagnostic.set_primary_message(format_args!( - "Got {defaults_count} default values but only {num_fields} field names" + "Expected `dict[str, Any]`, found `{}`", + namespace_type.display(db) )); - diagnostic.info("This will raise `TypeError` at runtime"); - } - - let defaults_count = defaults_count.min(num_fields); - let fields = field_names - .iter() - .enumerate() - .map(|(i, field_name)| { - let default = if defaults_count > 0 && i >= num_fields - defaults_count { - // Index into default_types: first default corresponds to first - // field that has a default. - let default_idx = i - (num_fields - defaults_count); - Some(default_types[default_idx]) - } else { - None - }; - NamedTupleField { - name: field_name.clone(), - ty: Type::any(), - default, - } - }) - .collect(); - - NamedTupleSpec::known(db, fields) - } - - fn infer_typing_namedtuple_fields(&mut self, fields_arg: &ast::Expr) -> NamedTupleSpec<'db> { - #[derive(Debug, Copy, Clone, PartialEq, Eq)] - enum SequenceKind { - List, - Tuple, } - let db = self.db(); - - // Get the elements from the list or tuple literal. - let (elements, field_arg_kind) = match fields_arg { - ast::Expr::List(list) => (&list.elts, SequenceKind::List), - ast::Expr::Tuple(tuple) => (&tuple.elts, SequenceKind::Tuple), - _ => { - self.infer_expression(fields_arg, TypeContext::default()); - if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { - let mut diagnostic = builder.into_diagnostic( - "Invalid argument to parameter `fields` of `NamedTuple()`", - ); - diagnostic.set_primary_message("`fields` must be a literal list or tuple"); - } - return NamedTupleSpec::unknown(db); + // Extract name and base classes. + let name = if let Some(literal) = name_type.as_string_literal() { + Name::new(literal.value(db)) + } else { + if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) + { + let mut diagnostic = + builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`"); + diagnostic.set_primary_message(format_args!( + "Expected `str`, found `{}`", + name_type.display(db) + )); } + Name::new_static("") }; - let mut fields = vec![]; - - for (i, element) in elements.iter().enumerate() { - // Each element should be a tuple or list like ("field_name", type) or ["field_name", type]. - let (field_spec_elts, field_spec_kind) = match element { - ast::Expr::Tuple(tuple) => (&tuple.elts, SequenceKind::Tuple), - ast::Expr::List(list) => (&list.elts, SequenceKind::List), - _ => { - self.infer_expression(element, TypeContext::default()); - for element in &elements[(i + 1)..] { - self.infer_expression(element, TypeContext::default()); - } - match field_arg_kind { - SequenceKind::List => { - self.store_expression_type( - fields_arg, - KnownClass::List.to_instance(db), - ); - } - SequenceKind::Tuple => self.store_expression_type( - fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), - ), - } - if let Some(builder) = - self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) - { - let mut diagnostic = builder.into_diagnostic( - "Invalid argument to parameter `fields` of `NamedTuple()`", - ); - diagnostic.set_primary_message( - "`fields` must be a sequence of literal lists or tuples", - ); - } - return NamedTupleSpec::unknown(db); - } - }; - - let [name_expr, declaration_expr] = &**field_spec_elts else { - self.infer_expression(element, TypeContext::default()); - for element in &elements[(i + 1)..] { - self.infer_expression(element, TypeContext::default()); - } - match field_arg_kind { - SequenceKind::List => { - self.store_expression_type(fields_arg, KnownClass::List.to_instance(db)); - } - SequenceKind::Tuple => self.store_expression_type( - fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), - ), - } - if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { - let mut diagnostic = builder.into_diagnostic( - "Invalid argument to parameter `fields` of `NamedTuple()`", - ); - diagnostic.set_primary_message( - "Each element in `fields` must be a length-2 tuple or list", - ); - } - return NamedTupleSpec::unknown(db); - }; - - let name_type = self.infer_expression(name_expr, TypeContext::default()); - let declared_type = self.infer_type_expression(declaration_expr); - - let element_type = match field_spec_kind { - SequenceKind::Tuple => Type::heterogeneous_tuple(db, [name_type, declared_type]), - SequenceKind::List => KnownClass::List.to_specialized_instance( - db, - &[UnionType::from_two_elements(db, name_type, declared_type)], - ), - }; - - self.store_expression_type(element, element_type); - - let Some(name) = name_type.as_string_literal() else { - for element in &elements[(i + 1)..] { - self.infer_expression(element, TypeContext::default()); - } - match field_arg_kind { - SequenceKind::List => { - self.store_expression_type(fields_arg, KnownClass::List.to_instance(db)); - } - SequenceKind::Tuple => self.store_expression_type( - fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), - ), - } - if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, name_expr) { - let mut diagnostic = - builder.into_diagnostic("Invalid `NamedTuple` field name definition"); - diagnostic.set_primary_message(format_args!( - "Expected a string literal for the field name, found `{}`", - name_type.display(db) - )); - } - return NamedTupleSpec::unknown(db); - }; + let scope = self.scope(); - let field = NamedTupleField { - name: Name::new(name.value(db)), - ty: declared_type, - default: None, - }; + // For assigned `type()` calls, bases inference is deferred to handle forward references + // and recursive references (e.g., `X = type("X", (tuple["X | None"],), {})`). + // This avoids expensive Salsa fixpoint iteration by deferring inference until the + // class type is already bound. For dangling calls, infer and extract bases eagerly + // (they'll be stored in the anchor and used for validation). + let explicit_bases = if definition.is_none() { + let bases_type = self.infer_expression(bases_arg, TypeContext::default()); + self.extract_explicit_bases(bases_arg, bases_type) + } else { + None + }; - fields.push(field); - } + // Create the anchor for identifying this dynamic class. + // - For assigned `type()` calls, the Definition uniquely identifies the class, + // and bases inference is deferred. + // - For dangling calls, compute a relative offset from the scope's node index, + // and store the explicit bases directly (since they were inferred eagerly). + let anchor = if let Some(def) = definition { + // Register for deferred inference to infer bases and validate later. + self.deferred.insert(def, self.multi_inference_state); + DynamicClassAnchor::Definition(def) + } else { + let call_node_index = call_expr.node_index().load(); + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("scope anchor should not be NodeIndex::NONE"); + let call_u32 = call_node_index + .as_u32() + .expect("call node should not be NodeIndex::NONE"); - let names: Vec = fields.iter().map(|f| f.name.clone()).collect(); + // Use [Unknown] as fallback if bases extraction failed (e.g., not a tuple). + let anchor_bases = explicit_bases + .clone() + .unwrap_or_else(|| Box::from([Type::unknown()])); - self.check_invalid_namedtuple_field_names(&names, fields_arg, NamedTupleKind::Typing); + DynamicClassAnchor::ScopeOffset { + scope, + offset: call_u32 - anchor_u32, + explicit_bases: anchor_bases, + } + }; - let spec = NamedTupleSpec::known(db, fields.into_boxed_slice()); - self.store_expression_type( - fields_arg, - Type::KnownInstance(KnownInstanceType::NamedTupleSpec(spec)), + let dynamic_class = DynamicClassLiteral::new( + db, + name.clone(), + anchor, + members, + has_dynamic_namespace, + None, ); - spec - } - /// Report diagnostics for invalid field names in a namedtuple definition. - fn check_invalid_namedtuple_field_names( - &self, - field_names: &[Name], - fields_arg: &ast::Expr, - kind: NamedTupleKind, - ) { - for (i, field_name) in field_names.iter().enumerate() { - // Check for duplicate field names. - if field_names[..i].iter().any(|f| f == field_name) - && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Duplicate field name `{field_name}` in `{kind}()`" - )); - diagnostic.set_primary_message(format_args!( - "Field `{field_name}` already defined; will raise `ValueError` at runtime" - )); + // For dangling calls, validate bases eagerly. For assigned calls, validation is + // deferred along with bases inference. + if let Some(explicit_bases) = &explicit_bases { + // Validate bases and collect disjoint bases for diagnostics. + let mut disjoint_bases = + self.validate_dynamic_type_bases(bases_arg, explicit_bases, &name); + + // Check for MRO errors. + if report_dynamic_mro_errors(&self.context, dynamic_class, call_expr, bases_arg) { + // MRO succeeded, check for instance-layout-conflict. + disjoint_bases.remove_redundant_entries(db); + if disjoint_bases.len() > 1 { + report_instance_layout_conflict( + &self.context, + dynamic_class.header_range(db), + bases_arg.as_tuple_expr().map(|tuple| tuple.elts.as_slice()), + &disjoint_bases, + ); + } } - if field_name.starts_with('_') - && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Field name `{field_name}` in `{kind}()` cannot start with an underscore" - )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); - } else if is_keyword(field_name) - && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Field name `{field_name}` in `{kind}()` cannot be a Python keyword" - )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); - } else if !is_identifier(field_name) - && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) + // Check for metaclass conflicts. + if let Err(DynamicMetaclassConflict { + metaclass1, + base1, + metaclass2, + base2, + }) = dynamic_class.try_metaclass(db) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Field name `{field_name}` in `{kind}()` is not a valid identifier" - )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + report_conflicting_metaclass_from_bases( + &self.context, + call_expr.into(), + dynamic_class.name(db), + metaclass1, + base1.display(db), + metaclass2, + base2.display(db), + ); } } + + Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) } /// Extract explicit base types from a bases tuple type. @@ -8084,315 +4479,75 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = for_statement; self.infer_target(target, iter, &|builder, tcx| { - // TODO: `infer_for_statement_definition` reports a diagnostic if `iter_ty` isn't iterable - // but only if the target is a name. We should report a diagnostic here if the target isn't a name: - // `for a.x in not_iterable: ... - builder - .infer_standalone_expression(iter, tcx) - .iterate(builder.db()) - .homogeneous_element_type(builder.db()) - }); - - self.infer_body(body); - self.infer_body(orelse); - } - - fn infer_for_statement_definition( - &mut self, - for_stmt: &ForStmtDefinitionKind<'db>, - definition: Definition<'db>, - ) { - let iterable = for_stmt.iterable(self.module()); - let target = for_stmt.target(self.module()); - - let loop_var_value_type = match for_stmt.target_kind() { - TargetKind::Sequence(unpack_position, unpack) => { - let unpacked = infer_unpack_types(self.db(), unpack); - if unpack_position == UnpackPosition::First { - self.context.extend(unpacked.diagnostics()); - } - - unpacked.expression_type(target) - } - TargetKind::Single => { - let iterable_type = - self.infer_standalone_expression(iterable, TypeContext::default()); - - iterable_type - .try_iterate_with_mode( - self.db(), - EvaluationMode::from_is_async(for_stmt.is_async()), - ) - .map(|tuple| tuple.homogeneous_element_type(self.db())) - .unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) - }) - } - }; - - self.store_expression_type(target, loop_var_value_type); - self.add_binding(target.into(), definition) - .insert(self, loop_var_value_type); - } - - fn infer_while_statement(&mut self, while_statement: &ast::StmtWhile) { - let ast::StmtWhile { - range: _, - node_index: _, - test, - body, - orelse, - } = while_statement; - - let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - - if let Err(err) = test_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, &**test); - } - - self.infer_body(body); - self.infer_body(orelse); - } - - fn infer_import_statement(&mut self, import: &ast::StmtImport) { - let ast::StmtImport { - names, - is_lazy: _, - range: _, - node_index: _, - } = import; - - for alias in names { - self.infer_definition(alias); - } - } - - fn report_unresolved_import( - &self, - import_node: AnyNodeRef<'_>, - range: TextRange, - level: u32, - module: Option<&str>, - module_name: Option<&ModuleName>, - ) { - let is_import_reachable = self.is_reachable(import_node); - - if !is_import_reachable { - return; - } - - if let Some(module_name) = &module_name - && (self - .settings() - .allowed_unresolved_imports - .matches(module_name) - .is_include() - || self - .settings() - .replace_imports_with_any - .matches(module_name) - .is_include()) - { - return; - } - - let Some(builder) = self.context.report_lint(&UNRESOLVED_IMPORT, range) else { - return; - }; - - let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot resolve imported module `{}`", - format_import_from_module(level, module) - )); - - if level == 0 { - if let Some(module_name) = module_name { - let program = Program::get(self.db()); - let typeshed_versions = program.search_paths(self.db()).typeshed_versions(); - - // Loop over ancestors in case we have info on the parent module but not submodule - for module_name in module_name.ancestors() { - if let Some(version_range) = typeshed_versions.exact(&module_name) { - // We know it is a stdlib module on *some* Python versions... - let python_version = program.python_version(self.db()); - if !version_range.contains(python_version) { - // ...But not on *this* Python version. - diagnostic.info(format_args!( - "The stdlib module `{module_name}` is only available on Python {version_range}", - version_range = version_range.diagnostic_display(), - )); - add_inferred_python_version_hint_to_diagnostic( - self.db(), - &mut diagnostic, - "resolving modules", - ); - return; - } - // We found the most precise answer we could, stop searching - break; - } - } - } - } else { - if let Some(better_level) = (0..level).rev().find(|reduced_level| { - let Ok(module_name) = ModuleName::from_identifier_parts( - self.db(), - self.file(), - module, - *reduced_level, - ) else { - return false; - }; - resolve_module(self.db(), self.file(), &module_name).is_some() - }) { - diagnostic - .help("The module can be resolved if the number of leading dots is reduced"); - diagnostic.help(format_args!( - "Did you mean `{}`?", - format_import_from_module(better_level, module) - )); - diagnostic.set_concise_message(format_args!( - "Cannot resolve imported module `{}` - did you mean `{}`?", - format_import_from_module(level, module), - format_import_from_module(better_level, module) - )); - } - } - - // Add search paths information to the diagnostic - // Use the same search paths function that is used in actual module resolution - let verbose = self.db().verbose(); - let search_paths = search_paths(self.db(), ModuleResolveMode::StubsAllowed); - - diagnostic.info(format_args!( - "Searched in the following paths during module resolution:" - )); - - let mut search_paths = search_paths.enumerate().peekable(); - - while let Some((index, path)) = search_paths.next() { - if index > 4 && !verbose && search_paths.peek().is_some() { - let more = search_paths.count() + 1; - diagnostic.info(format_args!( - " ... and {more} more paths. Run with `-v` to see all paths." - )); - break; - } - diagnostic.info(format_args!( - " {}. {} ({})", - index + 1, - path, - path.describe_kind() - )); - } + // TODO: `infer_for_statement_definition` reports a diagnostic if `iter_ty` isn't iterable + // but only if the target is a name. We should report a diagnostic here if the target isn't a name: + // `for a.x in not_iterable: ... + builder + .infer_standalone_expression(iter, tcx) + .iterate(builder.db()) + .homogeneous_element_type(builder.db()) + }); - diagnostic.info( - "make sure your Python environment is properly configured: \ - https://docs.astral.sh/ty/modules/#python-environment", - ); + self.infer_body(body); + self.infer_body(orelse); } - fn infer_import_definition( + fn infer_for_statement_definition( &mut self, - node: &ast::StmtImport, - alias: &ast::Alias, + for_stmt: &ForStmtDefinitionKind<'db>, definition: Definition<'db>, ) { - let ast::Alias { - range: _, - node_index: _, - name, - asname, - } = alias; - - // The name of the module being imported - let Some(full_module_name) = ModuleName::new(name) else { - tracing::debug!("Failed to resolve import due to invalid syntax"); - self.add_unknown_declaration_with_binding(alias.into(), definition); - return; - }; + let iterable = for_stmt.iterable(self.module()); + let target = for_stmt.target(self.module()); - if self - .settings() - .replace_imports_with_any - .matches(&full_module_name) - .is_include() - { - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(Type::any()), - ); - return; - } + let loop_var_value_type = match for_stmt.target_kind() { + TargetKind::Sequence(unpack_position, unpack) => { + let unpacked = infer_unpack_types(self.db(), unpack); + if unpack_position == UnpackPosition::First { + self.context.extend(unpacked.diagnostics()); + } - // Resolve the module being imported. - let Some(full_module_ty) = self.module_type_from_name(&full_module_name) else { - self.report_unresolved_import( - node.into(), - alias.range(), - 0, - Some(name), - Some(&full_module_name), - ); - self.add_unknown_declaration_with_binding(alias.into(), definition); - return; - }; + unpacked.expression_type(target) + } + TargetKind::Single => { + let iterable_type = + self.infer_standalone_expression(iterable, TypeContext::default()); - let binding_ty = if asname.is_some() { - // If we are renaming the imported module via an `as` clause, then we bind the resolved - // module's type to that name, even if that module is nested. - full_module_ty - } else if full_module_name.contains('.') { - // If there's no `as` clause and the imported module is nested, we're not going to bind - // the resolved module itself into the current scope; we're going to bind the top-most - // parent package of that module. - let topmost_parent_name = - ModuleName::new(full_module_name.components().next().unwrap()).unwrap(); - let Some(topmost_parent_ty) = self.module_type_from_name(&topmost_parent_name) else { - self.add_unknown_declaration_with_binding(alias.into(), definition); - return; - }; - topmost_parent_ty - } else { - // If there's no `as` clause and the imported module isn't nested, then the imported - // module _is_ what we bind into the current scope. - full_module_ty + iterable_type + .try_iterate_with_mode( + self.db(), + EvaluationMode::from_is_async(for_stmt.is_async()), + ) + .map(|tuple| tuple.homogeneous_element_type(self.db())) + .unwrap_or_else(|err| { + err.report_diagnostic(&self.context, iterable_type, iterable.into()); + err.fallback_element_type(self.db()) + }) + } }; - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(binding_ty), - ); + self.store_expression_type(target, loop_var_value_type); + self.add_binding(target.into(), definition) + .insert(self, loop_var_value_type); } - fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) { - let ast::StmtImportFrom { - module: _, - names, - level: _, - is_lazy: _, + fn infer_while_statement(&mut self, while_statement: &ast::StmtWhile) { + let ast::StmtWhile { range: _, node_index: _, - } = import; - - self.check_import_from_module_is_resolvable(import); - - for alias in names { - for definition in self.index.definitions(alias) { - let inferred = infer_definition_types(self.db(), *definition); - // Check non-star imports for deprecations - if definition.kind(self.db()).as_star_import().is_none() { - // In the initial cycle, `declaration_types()` is empty, so no deprecation check is performed. - for ty in inferred.declaration_types() { - self.check_deprecated(alias, ty.inner); - } - } - self.extend_definition(inferred); - } + test, + body, + orelse, + } = while_statement; + + let test_ty = self.infer_standalone_expression(test, TypeContext::default()); + + if let Err(err) = test_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, &**test); } + + self.infer_body(body); + self.infer_body(orelse); } fn infer_assert_statement(&mut self, assert: &ast::StmtAssert) { @@ -8445,392 +4600,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - /// Resolve the [`ModuleName`], and the type of the module, being referred to by an - /// [`ast::StmtImportFrom`] node. Emit a diagnostic if the module cannot be resolved. - fn check_import_from_module_is_resolvable(&mut self, import_from: &ast::StmtImportFrom) { - let ast::StmtImportFrom { module, level, .. } = import_from; - - // For diagnostics, we want to highlight the unresolvable - // module and not the entire `from ... import ...` statement. - let module_ref = module - .as_ref() - .map(AnyNodeRef::from) - .unwrap_or_else(|| AnyNodeRef::from(import_from)); - let module = module.as_deref(); - - tracing::trace!( - "Resolving import statement from module `{}` into file `{}`", - format_import_from_module(*level, module), - self.file().path(self.db()), - ); - let module_name = ModuleName::from_import_statement(self.db(), self.file(), import_from); - - let module_name = match module_name { - Ok(module_name) => module_name, - Err(ModuleNameResolutionError::InvalidSyntax) => { - tracing::debug!("Failed to resolve import due to invalid syntax"); - // Invalid syntax diagnostics are emitted elsewhere. - return; - } - Err(ModuleNameResolutionError::TooManyDots) => { - tracing::debug!( - "Relative module resolution `{}` failed: too many leading dots", - format_import_from_module(*level, module), - ); - self.report_unresolved_import( - import_from.into(), - module_ref.range(), - *level, - module, - None, - ); - return; - } - Err(ModuleNameResolutionError::UnknownCurrentModule) => { - tracing::debug!( - "Relative module resolution `{}` failed: could not resolve file `{}` to a module \ - (try adjusting configured search paths?)", - format_import_from_module(*level, module), - self.file().path(self.db()) - ); - self.report_unresolved_import( - import_from.into(), - module_ref.range(), - *level, - module, - None, - ); - return; - } - }; - - if resolve_module(self.db(), self.file(), &module_name).is_none() { - self.report_unresolved_import( - import_from.into(), - module_ref.range(), - *level, - module, - Some(&module_name), - ); - } - } - - fn infer_import_from_definition( - &mut self, - import_from: &ast::StmtImportFrom, - alias: &ast::Alias, - definition: Definition<'db>, - ) { - let Ok(module_name) = - ModuleName::from_import_statement(self.db(), self.file(), import_from) - else { - self.add_unknown_declaration_with_binding(alias.into(), definition); - return; - }; - - if self - .settings() - .replace_imports_with_any - .matches(&module_name) - .is_include() - { - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(Type::any()), - ); - return; - } - - let Some(module) = resolve_module(self.db(), self.file(), &module_name) else { - self.add_unknown_declaration_with_binding(alias.into(), definition); - return; - }; - - let module_ty = Type::module_literal(self.db(), self.file(), module); - - let name = if let Some(star_import) = definition.kind(self.db()).as_star_import() { - self.index - .place_table(self.scope().file_scope_id(self.db())) - .symbol(star_import.symbol_id()) - .name() - } else { - &alias.name.id - }; - - // Avoid looking up attributes on a module if a module imports from itself - // at the module-global scope, where the import definition itself is one of the - // bindings for the symbol being looked up, which would cause a query cycle. - // - // In nested scopes (e.g. function bodies), the module's global-scope definitions - // are resolved independently, so there is no cycle risk and the lookup is safe. - let skip_self_referential_member_lookup = module_ty - .as_module_literal() - .is_some_and(|module| Some(self.file()) == module.module(self.db()).file(self.db())) - && self.scope().file_scope_id(self.db()).is_global(); - - // Although it isn't the runtime semantics, we go to some trouble to prioritize a submodule - // over module `__getattr__`, because that's what other type checkers do. - let mut from_module_getattr = None; - - // First try loading the requested attribute from the module. - if !skip_self_referential_member_lookup { - if let PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - ty, - definedness: boundness, - .. - }), - qualifiers, - } = module_ty.member(self.db(), name) - { - if &alias.name != "*" && boundness == Definedness::PossiblyUndefined { - // TODO: Consider loading _both_ the attribute and any submodule and unioning them - // together if the attribute exists but is possibly-unbound. - if let Some(builder) = self - .context - .report_lint(&POSSIBLY_MISSING_IMPORT, AnyNodeRef::Alias(alias)) - { - builder.into_diagnostic(format_args!( - "Member `{name}` of module `{module_name}` may be missing", - )); - } - } - if qualifiers.contains(TypeQualifiers::FROM_MODULE_GETATTR) { - from_module_getattr = Some((ty, qualifiers)); - } else { - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::MightBeDifferent { - declared_ty: TypeAndQualifiers { - inner: ty, - origin: TypeOrigin::Declared, - qualifiers, - }, - inferred_ty: ty, - }, - ); - return; - } - } - } - - // Evaluate whether `X.Y` would constitute a valid submodule name, - // given a `from X import Y` statement. If it is valid, this will be `Some()`; - // else, it will be `None`. - let full_submodule_name = ModuleName::new(name).map(|final_part| { - let mut ret = module_name.clone(); - ret.extend(&final_part); - ret - }); - - // If the module doesn't bind the symbol, check if it's a submodule. This won't get - // handled by the `Type::member` call because it relies on the semantic index's - // `imported_modules` set. The semantic index does not include information about - // `from...import` statements because there are two things it cannot determine while only - // inspecting the content of the current file: - // - // - whether the imported symbol is an attribute or submodule - // - whether the containing file is in a module or a package (needed to correctly resolve - // relative imports) - // - // The first would be solvable by making it a _potentially_ imported modules set. The - // second is not. - // - // Regardless, for now, we sidestep all of that by repeating the submodule-or-attribute - // check here when inferring types for a `from...import` statement. - if let Some(submodule_type) = full_submodule_name - .as_ref() - .and_then(|submodule_name| self.module_type_from_name(submodule_name)) - { - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::are_the_same_type(submodule_type), - ); - return; - } - - // We've checked for a submodule, so now we can go ahead and use a type from module - // `__getattr__`. - if let Some((ty, qualifiers)) = from_module_getattr { - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::MightBeDifferent { - declared_ty: TypeAndQualifiers { - inner: ty, - origin: TypeOrigin::Declared, - qualifiers, - }, - inferred_ty: ty, - }, - ); - return; - } - - self.add_unknown_declaration_with_binding(alias.into(), definition); - - if &alias.name == "*" { - return; - } - - if !self.is_reachable(import_from) { - return; - } - - if self - .settings() - .allowed_unresolved_imports - .matches(full_submodule_name.as_ref().unwrap_or(&module_name)) - .is_include() - { - return; - } - - let Some(builder) = self - .context - .report_lint(&UNRESOLVED_IMPORT, AnyNodeRef::Alias(alias)) - else { - return; - }; - - let mut diagnostic = builder.into_diagnostic(format_args!( - "Module `{module_name}` has no member `{name}`" - )); - - let mut submodule_hint_added = false; - - if let Some(full_submodule_name) = full_submodule_name { - submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions( - self.db(), - &mut diagnostic, - &full_submodule_name, - module, - ); - } - - if !submodule_hint_added { - hint_if_stdlib_attribute_exists_on_other_versions( - self.db(), - diagnostic, - module_ty, - name, - "resolving imports", - ); - } - } - - /// Infer the implicit local definition `x = ` that - /// `from .x.y import z` or `from whatever.thispackage.x.y` can introduce in `__init__.py(i)`. - /// - /// For the definition `z`, see [`TypeInferenceBuilder::infer_import_from_definition`]. - /// - /// The runtime semantic of this kind of statement is to introduce a variable in the global - /// scope of this module *the first time it's imported in the entire program*. This - /// implementation just blindly introduces a local variable wherever the `from..import` is - /// (if the imports actually resolve). - /// - /// That gap between the semantics and implementation are currently the responsibility of the - /// code that actually creates these kinds of Definitions (so blindly introducing a local - /// is all we need to be doing here). - fn infer_import_from_submodule_definition( - &mut self, - import_from: &'ast ast::StmtImportFrom, - definition: Definition<'db>, - ) { - // Get this package's absolute module name by resolving `.`, and make sure it exists - let Ok(thispackage_name) = ModuleName::package_for_file(self.db(), self.file()) else { - self.add_binding(import_from.into(), definition) - .insert(self, Type::unknown()); - return; - }; - - let Some(module) = resolve_module(self.db(), self.file(), &thispackage_name) else { - self.add_binding(import_from.into(), definition) - .insert(self, Type::unknown()); - return; - }; - - // We have `from whatever.thispackage.x.y ...` or `from .x.y ...` - // and we want to extract `x` (to ultimately construct `whatever.thispackage.x`): - - // First we normalize to `whatever.thispackage.x.y` - let Some(final_part) = ModuleName::from_identifier_parts( - self.db(), - self.file(), - import_from.module.as_deref(), - import_from.level, - ) - .ok() - // `whatever.thispackage.x.y` => `x.y` - .and_then(|submodule_name| submodule_name.relative_to(&thispackage_name)) - // `x.y` => `x` - .and_then(|relative_submodule_name| { - relative_submodule_name - .components() - .next() - .and_then(ModuleName::new) - }) else { - self.add_binding(import_from.into(), definition) - .insert(self, Type::unknown()); - return; - }; - - // `x` => `whatever.thispackage.x` - let mut full_submodule_name = thispackage_name.clone(); - full_submodule_name.extend(&final_part); - - // Try to actually resolve the import `whatever.thispackage.x` - if let Some(submodule_type) = self.module_type_from_name(&full_submodule_name) { - // Success, introduce a binding! - // - // We explicitly don't introduce a *declaration* because it's actual ok - // (and fairly common) to overwrite this import with a function or class - // and we don't want it to be a type error to do so. - self.add_binding(import_from.into(), definition) - .insert(self, submodule_type); - return; - } - - // That didn't work, try to produce diagnostics - self.add_binding(import_from.into(), definition) - .insert(self, Type::unknown()); - - if self - .settings() - .allowed_unresolved_imports - .matches(&full_submodule_name) - .is_include() - { - return; - } - - if !self.is_reachable(import_from) { - return; - } - - let Some(builder) = self - .context - .report_lint(&UNRESOLVED_IMPORT, AnyNodeRef::StmtImportFrom(import_from)) - else { - return; - }; - - let mut diagnostic = builder.into_diagnostic(format_args!( - "Module `{thispackage_name}` has no submodule `{final_part}`" - )); - - hint_if_stdlib_submodule_exists_on_other_versions( - self.db(), - &mut diagnostic, - &full_submodule_name, - module, - ); - } - fn infer_return_statement(&mut self, ret: &ast::StmtReturn) { let tcx = if ret.value.is_some() { nearest_enclosing_function(self.db(), self.index, self.scope()) @@ -9732,7 +5501,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - fn infer_number_literal_expression(&mut self, literal: &ast::ExprNumberLiteral) -> Type<'db> { + fn infer_number_literal_expression(&self, literal: &ast::ExprNumberLiteral) -> Type<'db> { let ast::ExprNumberLiteral { range: _, node_index: _, @@ -9751,7 +5520,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } #[expect(clippy::unused_self)] - fn infer_boolean_literal_expression(&mut self, literal: &ast::ExprBooleanLiteral) -> Type<'db> { + fn infer_boolean_literal_expression(&self, literal: &ast::ExprBooleanLiteral) -> Type<'db> { let ast::ExprBooleanLiteral { range: _, node_index: _, @@ -13164,14 +8933,6 @@ impl From for DeferredExpressionState { } } -fn format_import_from_module(level: u32, module: Option<&str>) -> String { - format!( - "{}{}", - ".".repeat(level as usize), - module.unwrap_or_default() - ) -} - /// Struct collecting string parts when inferring a formatted string. Infers a string literal if the /// concatenated string is small enough, otherwise infers a literal string. /// @@ -13229,21 +8990,6 @@ impl StringPartsCollector { } } -fn contains_string_literal(expr: &ast::Expr) -> bool { - struct ContainsStringLiteral(bool); - - impl<'a> Visitor<'a> for ContainsStringLiteral { - fn visit_expr(&mut self, expr: &'a ast::Expr) { - self.0 |= matches!(expr, ast::Expr::StringLiteral(_)); - walk_expr(self, expr); - } - } - - let mut visitor = ContainsStringLiteral(false); - visitor.visit_expr(expr); - visitor.0 -} - /// Map based on a `Vec`. It doesn't enforce /// uniqueness on insertion. Instead, it relies on the caller /// that elements are unique. For example, the way we visit definitions @@ -13574,41 +9320,6 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum NamedTupleKind { - Collections, - Typing, -} - -impl NamedTupleKind { - const fn is_collections(self) -> bool { - matches!(self, Self::Collections) - } - - const fn is_typing(self) -> bool { - matches!(self, Self::Typing) - } - - fn from_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { - match ty { - Type::SpecialForm(SpecialFormType::NamedTuple) => Some(NamedTupleKind::Typing), - Type::FunctionLiteral(function) => function - .is_known(db, KnownFunction::NamedTuple) - .then_some(NamedTupleKind::Collections), - _ => None, - } - } -} - -impl std::fmt::Display for NamedTupleKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - NamedTupleKind::Collections => "namedtuple", - NamedTupleKind::Typing => "NamedTuple", - }) - } -} - #[derive(Copy, Clone, Debug)] enum BoundOrConstraintsNodes<'ast> { Bound(&'ast ast::Expr), diff --git a/crates/ty_python_semantic/src/types/infer/builder/class.rs b/crates/ty_python_semantic/src/types/infer/builder/class.rs new file mode 100644 index 0000000000000..824ef6ce331fc --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/class.rs @@ -0,0 +1,257 @@ +use crate::{ + semantic_index::{definition::Definition, scope::NodeWithScopeRef}, + types::{ + CallArguments, DataclassParams, KnownClass, KnownInstanceType, SpecialFormType, + StaticClassLiteral, Type, TypeContext, + call::CallError, + function::KnownFunction, + infer::{ + TypeInferenceBuilder, + builder::{DeclaredAndInferredType, DeferredExpressionState}, + }, + signatures::ParameterForm, + }, +}; +use ruff_python_ast::{self as ast, helpers::any_over_expr}; +use ty_module_resolver::{KnownModule, file_to_module}; + +impl<'db> TypeInferenceBuilder<'db, '_> { + pub(super) fn infer_class_body(&mut self, class: &ast::StmtClassDef) { + self.infer_body(&class.body); + } + + pub(super) fn infer_class_type_params(&mut self, class: &ast::StmtClassDef) { + let type_params = class + .type_params + .as_deref() + .expect("class type params scope without type params"); + + let binding_context = self.index.expect_single_definition(class); + let previous_typevar_binding_context = + self.typevar_binding_context.replace(binding_context); + + self.infer_type_parameters(type_params); + + if let Some(arguments) = class.arguments.as_deref() { + let in_stub = self.in_stub(); + let previous_deferred_state = + std::mem::replace(&mut self.deferred_state, in_stub.into()); + let mut call_arguments = + CallArguments::from_arguments(arguments, |argument, splatted_value| { + let ty = self.infer_expression(splatted_value, TypeContext::default()); + if let Some(argument) = argument { + self.store_expression_type(argument, ty); + } + ty + }); + let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; + self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); + self.deferred_state = previous_deferred_state; + } + + self.typevar_binding_context = previous_typevar_binding_context; + } + + pub(super) fn infer_class_definition_statement(&mut self, class: &ast::StmtClassDef) { + self.infer_definition(class); + } + + pub(super) fn infer_class_definition( + &mut self, + class_node: &ast::StmtClassDef, + definition: Definition<'db>, + ) { + let ast::StmtClassDef { + range: _, + node_index: _, + name, + type_params, + decorator_list, + arguments: _, + body: _, + } = class_node; + let db = self.db(); + + let mut decorator_types_and_nodes: Vec<(Type<'db>, &ast::Decorator)> = + Vec::with_capacity(decorator_list.len()); + let mut deprecated = None; + let mut type_check_only = false; + let mut dataclass_params = None; + let mut dataclass_transformer_params = None; + let mut total_ordering = false; + for decorator in decorator_list { + let decorator_ty = self.infer_decorator(decorator); + if decorator_ty + .as_function_literal() + .is_some_and(|function| function.is_known(db, KnownFunction::Dataclass)) + { + dataclass_params = Some(DataclassParams::default_params(db)); + continue; + } + + if decorator_ty + .as_function_literal() + .is_some_and(|function| function.is_known(db, KnownFunction::TotalOrdering)) + { + total_ordering = true; + continue; + } + + if let Type::DataclassDecorator(params) = decorator_ty { + dataclass_params = Some(params); + continue; + } + + if let Type::KnownInstance(KnownInstanceType::Deprecated(deprecated_inst)) = + decorator_ty + { + deprecated = Some(deprecated_inst); + continue; + } + + if decorator_ty + .as_function_literal() + .is_some_and(|function| function.is_known(db, KnownFunction::TypeCheckOnly)) + { + type_check_only = true; + continue; + } + + // Skip identity decorators to avoid salsa cycles on typeshed. + if decorator_ty.as_function_literal().is_some_and(|function| { + matches!( + function.known(db), + Some( + KnownFunction::Final + | KnownFunction::DisjointBase + | KnownFunction::RuntimeCheckable + ) + ) + }) { + continue; + } + + if let Type::FunctionLiteral(f) = decorator_ty { + // We do not yet detect or flag `@dataclass_transform` applied to more than one + // overload, or an overload and the implementation both. Nevertheless, this is not + // allowed. We do not try to treat the offenders intelligently -- just use the + // params of the last seen usage of `@dataclass_transform` + let transformer_params = f + .iter_overloads_and_implementation(db) + .rev() + .find_map(|overload| overload.dataclass_transformer_params(db)); + if let Some(transformer_params) = transformer_params { + dataclass_params = Some(DataclassParams::from_transformer_params( + db, + transformer_params, + )); + continue; + } + } + + if let Type::DataclassTransformer(params) = decorator_ty { + dataclass_transformer_params = Some(params); + continue; + } + + decorator_types_and_nodes.push((decorator_ty, decorator)); + } + + let body_scope = self + .index + .node_scope(NodeWithScopeRef::Class(class_node)) + .to_scope_id(db, self.file()); + + let maybe_known_class = KnownClass::try_from_file_and_name(db, self.file(), name); + + let in_typing_module = || { + matches!( + file_to_module(db, self.file()).and_then(|module| module.known(db)), + Some(KnownModule::Typing | KnownModule::TypingExtensions) + ) + }; + + let inferred_ty = match (maybe_known_class, &*name.id) { + (None, "NamedTuple") if in_typing_module() => { + Type::SpecialForm(SpecialFormType::NamedTuple) + } + (None, "Any") if in_typing_module() => Type::SpecialForm(SpecialFormType::Any), + _ => Type::from(StaticClassLiteral::new( + db, + name.id.clone(), + body_scope, + maybe_known_class, + deprecated, + type_check_only, + dataclass_params, + dataclass_transformer_params, + total_ordering, + )), + }; + + // Validate decorator calls (but don't use return types yet). + for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { + if let Err(CallError(_, bindings)) = + decorator_ty.try_call(db, &CallArguments::positional([inferred_ty])) + { + bindings.report_diagnostics(&self.context, (*decorator_node).into()); + } + } + + self.add_declaration_with_binding( + class_node.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(inferred_ty), + ); + + // if there are type parameters, then the keywords and bases are within that scope + // and we don't need to run inference here + if type_params.is_none() { + // In stub files, keyword values may reference names that are defined later in the file. + let in_stub = self.in_stub(); + let previous_deferred_state = + std::mem::replace(&mut self.deferred_state, in_stub.into()); + for keyword in class_node.keywords() { + self.infer_expression(&keyword.value, TypeContext::default()); + } + self.deferred_state = previous_deferred_state; + + // Inference of bases deferred in stubs, or if any are string literals. + if self.in_stub() + || class_node + .bases() + .iter() + .any(|expr| any_over_expr(expr, &ast::Expr::is_string_literal_expr)) + { + self.deferred.insert(definition, self.multi_inference_state); + } else { + let previous_typevar_binding_context = + self.typevar_binding_context.replace(definition); + for base in class_node.bases() { + self.infer_expression(base, TypeContext::default()); + } + self.typevar_binding_context = previous_typevar_binding_context; + } + } + } + + pub(super) fn infer_class_deferred( + &mut self, + definition: Definition<'db>, + class: &ast::StmtClassDef, + ) { + let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); + for base in class.bases() { + if self.in_stub() { + self.infer_expression_with_state( + base, + TypeContext::default(), + DeferredExpressionState::Deferred, + ); + } else { + self.infer_expression(base, TypeContext::default()); + } + } + self.typevar_binding_context = previous_typevar_binding_context; + } +} diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs new file mode 100644 index 0000000000000..090939036a98b --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -0,0 +1,884 @@ +use crate::{ + TypeQualifiers, + semantic_index::{ + definition::{Definition, DefinitionKind}, + scope::NodeWithScopeRef, + }, + types::{ + KnownClass, KnownInstanceType, ParamSpecAttrKind, SubclassOfInner, SubclassOfType, Type, + TypeContext, UnionType, + context::InNoTypeCheck, + diagnostic::{ + FINAL_ON_NON_METHOD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_TYPE_FORM, + USELESS_OVERLOAD_BODY, add_type_expression_reference_link, + is_invalid_typed_dict_literal, report_implicit_return_type, + report_invalid_generator_function_return_type, report_invalid_return_type, + report_shadowed_type_variable, + }, + function::{ + FunctionBodyKind, FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, + OverloadLiteral, function_body_kind, is_implicit_classmethod, + }, + generics::{enclosing_generic_contexts, typing_self}, + infer::{ + TypeInferenceBuilder, + builder::{ + DeclaredAndInferredType, DeferredExpressionState, TypeAndRange, + validate_paramspec_components, + }, + nearest_enclosing_function, + }, + infer_definition_types, infer_scope_types, todo_type, + }, +}; + +use ruff_python_ast as ast; +use ruff_text_size::Ranged; + +impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { + pub(super) fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) { + let db = self.db(); + + // Parameters are odd: they are Definitions in the function body scope, but have no + // constituent nodes that are part of the function body. In order to get diagnostics + // merged/emitted for them, we need to explicitly infer their definitions here. + for parameter in &function.parameters { + self.infer_definition(parameter); + } + + validate_paramspec_components(&self.context, &function.parameters, |expr| { + self.file_expression_type(expr) + }); + + self.infer_body(&function.body); + + if let Some(returns) = function.returns.as_deref() { + let has_empty_body = self.return_types_and_ranges.is_empty() + && function_body_kind(db, function, |expr| self.expression_type(expr)) + == FunctionBodyKind::Stub; + + let mut enclosing_class_context = None; + + if has_empty_body { + if self.in_stub() { + return; + } + if self.in_function_overload_or_abstractmethod() { + return; + } + if self.scope().scope(db).in_type_checking_block() { + return; + } + if let Some(class) = self.class_context_of_current_method() { + enclosing_class_context = Some(class); + if class.is_protocol(db) { + return; + } + } + } + + let enclosing_function = nearest_enclosing_function(db, self.index, self.scope()) + .expect("should be in a function body scope"); + let declared_ty = enclosing_function + .last_definition_raw_signature(db) + .return_ty; + let expected_ty = match declared_ty { + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(db), + ty => ty, + }; + + let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); + if scope_id.is_generator_function(self.index) { + // TODO: `AsyncGeneratorType` and `GeneratorType` are both generic classes. + // + // If type arguments are supplied to `(Async)Iterable`, `(Async)Iterator`, + // `(Async)Generator` or `(Async)GeneratorType` in the return annotation, + // we should iterate over the `yield` expressions and `return` statements + // in the function to check that they are consistent with the type arguments + // provided. Once we do this, the `.to_instance_unknown` call below should + // be replaced with `.to_specialized_instance`. + let inferred_return = if function.is_async { + KnownClass::AsyncGeneratorType + } else { + KnownClass::GeneratorType + }; + + if !inferred_return + .to_instance_unknown(db) + .is_assignable_to(db, expected_ty) + { + report_invalid_generator_function_return_type( + &self.context, + returns.range(), + inferred_return, + declared_ty, + ); + } + return; + } + + for invalid in self + .return_types_and_ranges + .iter() + .copied() + .filter_map(|ty_range| match ty_range.ty { + // We skip `is_assignable_to` checks for `NotImplemented`, + // so we remove it beforehand. + Type::Union(union) => Some(TypeAndRange { + ty: union.filter(db, |ty| !ty.is_notimplemented(db)), + range: ty_range.range, + }), + ty if ty.is_notimplemented(db) => None, + _ => Some(ty_range), + }) + .filter(|ty_range| !ty_range.ty.is_assignable_to(db, expected_ty)) + { + report_invalid_return_type( + &self.context, + invalid.range, + returns.range(), + declared_ty, + invalid.ty, + ); + } + if self + .index + .use_def_map(scope_id) + .can_implicitly_return_none(db) + && !Type::none(db).is_assignable_to(db, expected_ty) + { + let no_return = self.return_types_and_ranges.is_empty(); + report_implicit_return_type( + &self.context, + returns.range(), + declared_ty, + has_empty_body, + enclosing_class_context, + no_return, + ); + } + } + } + + pub(super) fn infer_function_definition_statement(&mut self, function: &ast::StmtFunctionDef) { + self.infer_definition(function); + } + + pub(super) fn infer_function_definition( + &mut self, + function: &ast::StmtFunctionDef, + definition: Definition<'db>, + ) { + let ast::StmtFunctionDef { + range: _, + node_index: _, + is_async: _, + name, + type_params, + parameters, + returns: _, + body: _, + decorator_list, + } = function; + + let db = self.db(); + + let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len()); + let mut function_decorators = FunctionDecorators::empty(); + let mut deprecated = None; + let mut dataclass_transformer_params = None; + let mut final_decorator = None; + + for decorator in decorator_list { + let decorator_type = self.infer_decorator(decorator); + let decorator_function_decorator = + FunctionDecorators::from_decorator_type(db, decorator_type); + function_decorators |= decorator_function_decorator; + + match decorator_type { + Type::FunctionLiteral(function) => match function.known(db) { + Some(KnownFunction::NoTypeCheck) => { + // If the function is decorated with the `no_type_check` decorator, + // we need to suppress any errors that come after the decorators. + self.context.set_in_no_type_check(InNoTypeCheck::Yes); + continue; + } + Some(KnownFunction::Final) => { + final_decorator = Some(decorator); + continue; + } + _ => {} + }, + Type::KnownInstance(KnownInstanceType::Deprecated(deprecated_inst)) => { + deprecated = Some(deprecated_inst); + } + Type::DataclassTransformer(params) => { + dataclass_transformer_params = Some(params); + } + _ => {} + } + if !decorator_function_decorator.is_empty() { + continue; + } + + decorator_types_and_nodes.push((decorator_type, decorator)); + } + + // Check for `@final` applied to non-method functions. + // `@final` is only meaningful on methods and classes. + if let Some(final_decorator) = final_decorator + && !self + .index + .scope(self.scope().file_scope_id(db)) + .kind() + .is_class() + && let Some(builder) = self + .context + .report_lint(&FINAL_ON_NON_METHOD, final_decorator) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@final` cannot be applied to non-method function `{name}`", + )); + diagnostic.info("`@final` is only meaningful on methods and classes"); + } + + let has_defaults = parameters + .iter_non_variadic_params() + .any(|param| param.default.is_some()); + + // If there are type params, parameters and returns are evaluated in that scope. Otherwise, + // we always defer the inference of the parameters and returns. That ensures that we do not + // add any spurious salsa cycles when applying decorators below. (Applying a decorator + // requires getting the signature of this function definition, which in turn requires + // (lazily) inferring the parameter and return types.) If defaults exist, we also defer so + // they can be inferred once with type context in the enclosing scope. + if type_params.is_none() || has_defaults { + self.deferred.insert(definition, self.multi_inference_state); + } + + let known_function = KnownFunction::try_from_definition_and_name(db, definition, name); + + // `type_check_only` is itself not available at runtime + if known_function == Some(KnownFunction::TypeCheckOnly) { + function_decorators |= FunctionDecorators::TYPE_CHECK_ONLY; + } + + let body_scope = self + .index + .node_scope(NodeWithScopeRef::Function(function)) + .to_scope_id(db, self.file()); + + let overload_literal = OverloadLiteral::new( + db, + &name.id, + known_function, + body_scope, + function_decorators, + deprecated, + dataclass_transformer_params, + ); + let function_literal = FunctionLiteral::new(db, overload_literal); + + let mut inferred_ty = + Type::FunctionLiteral(FunctionType::new(db, function_literal, None, None)); + self.undecorated_type = Some(inferred_ty); + + // Check that the function's own type parameters don't shadow + // type variables from enclosing scopes (by name). + if let Some(type_params) = &function.type_params { + let current_scope = self.scope().file_scope_id(db); + for type_param in type_params.iter() { + let param_name = type_param.name(); + for enclosing in enclosing_generic_contexts(db, self.index, current_scope) { + if let Some(other_typevar) = enclosing.binds_named_typevar(db, ¶m_name.id) { + report_shadowed_type_variable( + &self.context, + ¶m_name.id, + "function", + &function.name.id, + function.name.range(), + other_typevar, + ); + } + } + } + } + + for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { + inferred_ty = self.apply_decorator(*decorator_ty, inferred_ty, decorator_node); + } + + self.add_declaration_with_binding( + function.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(inferred_ty), + ); + + if function_decorators.contains(FunctionDecorators::OVERLOAD) { + for stmt in &function.body { + match stmt { + ast::Stmt::Pass(_) => continue, + ast::Stmt::Expr(ast::StmtExpr { value, .. }) => { + if matches!( + &**value, + ast::Expr::StringLiteral(_) | ast::Expr::EllipsisLiteral(_) + ) { + continue; + } + } + _ => {} + } + let Some(builder) = self.context.report_lint(&USELESS_OVERLOAD_BODY, stmt) else { + continue; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Useless body for `@overload`-decorated function `{}`", + &function.name + )); + diagnostic.set_primary_message("This statement will never be executed"); + diagnostic.info( + "`@overload`-decorated functions are solely for type checkers \ + and must be overwritten at runtime by a non-`@overload`-decorated implementation", + ); + diagnostic.help("Consider replacing this function body with `...` or `pass`"); + break; + } + } + } + + pub(super) fn infer_function_deferred( + &mut self, + definition: Definition<'db>, + function: &ast::StmtFunctionDef, + ) { + let db = self.db(); + let mut prev_in_no_type_check = self.context.set_in_no_type_check(InNoTypeCheck::Yes); + for decorator in &function.decorator_list { + let decorator_type = self.infer_decorator(decorator); + if let Type::FunctionLiteral(function) = decorator_type + && let Some(KnownFunction::NoTypeCheck) = function.known(db) + { + // If the function is decorated with the `no_type_check` decorator, + // we need to suppress any errors that come after the decorators. + prev_in_no_type_check = InNoTypeCheck::Yes; + break; + } + } + self.context.set_in_no_type_check(prev_in_no_type_check); + + let has_type_params = function.type_params.is_some(); + let has_defaults = function + .parameters + .iter_non_variadic_params() + .any(|param| param.default.is_some()); + + let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); + + if !has_type_params { + self.infer_return_type_annotation( + function.returns.as_deref(), + self.defer_annotations().into(), + ); + self.infer_parameters(function.parameters.as_ref()); + } + + if has_defaults { + // In stub files, default values may reference names that are defined later in the file. + let in_stub = self.in_stub(); + let previous_deferred_state = + std::mem::replace(&mut self.deferred_state, in_stub.into()); + + // For generic functions, only defaults are inferred here; annotation types come from + // the type-params scope. + if has_type_params { + let type_params_scope = self + .index + .node_scope(NodeWithScopeRef::FunctionTypeParameters(function)) + .to_scope_id(db, self.file()); + let type_params_inference = + infer_scope_types(db, type_params_scope, TypeContext::default()); + + for param_with_default in function.parameters.iter_non_variadic_params() { + let Some(default) = param_with_default.default.as_deref() else { + continue; + }; + let tcx = param_with_default + .parameter + .annotation + .as_deref() + .map(|annotation| { + TypeContext::new(Some( + type_params_inference.expression_type(annotation), + )) + }) + .unwrap_or_else(TypeContext::default); + self.infer_expression(default, tcx); + } + } else { + for param_with_default in function.parameters.iter_non_variadic_params() { + let Some(default) = param_with_default.default.as_deref() else { + continue; + }; + let tcx = param_with_default + .parameter + .annotation + .as_deref() + .map(|annotation| TypeContext::new(Some(self.expression_type(annotation)))) + .unwrap_or_else(TypeContext::default); + self.infer_expression(default, tcx); + } + } + + self.deferred_state = previous_deferred_state; + } + + self.typevar_binding_context = previous_typevar_binding_context; + } + + fn infer_return_type_annotation( + &mut self, + returns: Option<&ast::Expr>, + deferred_expression_state: DeferredExpressionState, + ) { + let Some(returns) = returns else { + return; + }; + let annotated = self.infer_annotation_expression(returns, deferred_expression_state); + + if annotated.qualifiers.is_empty() { + return; + } + for qualifier in [ + TypeQualifiers::FINAL, + TypeQualifiers::CLASS_VAR, + TypeQualifiers::INIT_VAR, + ] { + if annotated.qualifiers.contains(qualifier) + && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, returns) + { + builder.into_diagnostic(format!( + "`{name}` is not allowed in function return type annotations", + name = qualifier.name() + )); + } + } + } + + pub(super) fn infer_function_type_params(&mut self, function: &ast::StmtFunctionDef) { + let type_params = function + .type_params + .as_deref() + .expect("function type params scope without type params"); + + let binding_context = self.index.expect_single_definition(function); + let previous_typevar_binding_context = + self.typevar_binding_context.replace(binding_context); + self.infer_return_type_annotation( + function.returns.as_deref(), + self.defer_annotations().into(), + ); + self.infer_type_parameters(type_params); + self.infer_parameters(&function.parameters); + self.typevar_binding_context = previous_typevar_binding_context; + } + + fn infer_parameters(&mut self, parameters: &ast::Parameters) { + let ast::Parameters { + range: _, + node_index: _, + posonlyargs: _, + args: _, + vararg, + kwonlyargs: _, + kwarg, + } = parameters; + + for param_with_default in parameters.iter_non_variadic_params() { + self.infer_parameter_with_default(param_with_default); + } + if let Some(vararg) = vararg { + self.inferring_vararg_annotation = true; + self.infer_parameter(vararg); + self.inferring_vararg_annotation = false; + } + if let Some(kwarg) = kwarg { + self.infer_parameter(kwarg); + } + } + + fn infer_parameter_with_default(&mut self, parameter_with_default: &ast::ParameterWithDefault) { + let ast::ParameterWithDefault { + range: _, + node_index: _, + parameter, + default: _, + } = parameter_with_default; + + let annotated = self.infer_optional_annotation_expression( + parameter.annotation.as_deref(), + self.defer_annotations().into(), + ); + + let Some(annotated) = annotated else { + return; + }; + + let qualifiers = annotated.qualifiers; + + if qualifiers.is_empty() { + return; + } + + for qualifier in [ + TypeQualifiers::FINAL, + TypeQualifiers::CLASS_VAR, + TypeQualifiers::INIT_VAR, + TypeQualifiers::REQUIRED, + TypeQualifiers::NOT_REQUIRED, + TypeQualifiers::READ_ONLY, + ] { + if qualifiers.contains(qualifier) + && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, parameter) + { + builder.into_diagnostic(format!( + "`{name}` is not allowed in function parameter annotations", + name = qualifier.name() + )); + } + } + } + + fn infer_parameter(&mut self, parameter: &ast::Parameter) { + let ast::Parameter { + range: _, + node_index: _, + name: _, + annotation, + } = parameter; + + self.infer_optional_annotation_expression( + annotation.as_deref(), + self.defer_annotations().into(), + ); + } + + /// Set initial declared type (if annotated) and inferred type for a function-parameter symbol, + /// in the function body scope. + /// + /// The declared type is the annotated type, if any, or `Unknown`. + /// + /// The inferred type is the annotated type, if any. If there is no annotation, it is the union + /// of `Unknown` and the type of the default value, if any. + /// + /// Parameter definitions are odd in that they define a symbol in the function-body scope, so + /// the Definition belongs to the function body scope, but the expressions (annotation and + /// default value) both belong to outer scopes. (The default value always belongs to the outer + /// scope in which the function is defined, the annotation belongs either to the outer scope, + /// or maybe to an intervening type-params scope, if it's a generic function.) So we don't use + /// `self.infer_expression` or store any expression types here, we just query for the types of + /// the expressions from their respective scopes. + /// + /// It is safe (non-cycle-causing) to query the annotation type via `file_expression_type` + /// here, because an outer scope can't depend on a definition from an inner scope, so we + /// shouldn't be in-process of inferring the outer scope here. + pub(super) fn infer_parameter_definition( + &mut self, + parameter_with_default: &'ast ast::ParameterWithDefault, + definition: Definition<'db>, + ) { + let ast::ParameterWithDefault { + parameter, + default, + range: _, + node_index: _, + } = parameter_with_default; + + let db = self.db(); + + let default_expr = default.as_ref(); + if let Some(annotation) = parameter.annotation.as_ref() { + let declared_ty = self.file_expression_type(annotation); + + // P.args and P.kwargs are only valid as annotations on *args and **kwargs, + // not on regular parameters. + if let Type::TypeVar(typevar) = declared_ty + && typevar.is_paramspec(db) + && let Some(attr) = typevar.paramspec_attr(db) + { + let name = typevar.name(db); + let (attr_name, variadic) = match attr { + ParamSpecAttrKind::Args => ("args", "*args"), + ParamSpecAttrKind::Kwargs => ("kwargs", "**kwargs"), + }; + if let Some(builder) = self + .context + .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) + { + builder.into_diagnostic(format_args!( + "`{name}.{attr_name}` is only valid for annotating `{variadic}`", + )); + } + } + + if let Some(default_expr) = default_expr { + let default_expr = default_expr.as_ref(); + let default_ty = self.file_expression_type(default_expr); + + // Avoid duplicate diagnostics: invalid TypedDict literals already emit specific errors. + let suppress_invalid_default = + is_invalid_typed_dict_literal(db, declared_ty, default_expr.into()); + if !default_ty.is_assignable_to(db, declared_ty) + && !suppress_invalid_default + && !((self.in_stub() + || self.in_function_overload_or_abstractmethod() + || self.scope().scope(db).in_type_checking_block() + || self + .class_context_of_current_method() + .is_some_and(|class| class.is_protocol(db))) + && default + .as_ref() + .is_some_and(|d| d.is_ellipsis_literal_expr())) + { + if let Some(builder) = self + .context + .report_lint(&INVALID_PARAMETER_DEFAULT, parameter_with_default) + { + builder.into_diagnostic(format_args!( + "Default value of type `{}` is not assignable \ + to annotated parameter type `{}`", + default_ty.display(db), + declared_ty.display(db) + )); + } + } + } + + self.add_declaration_with_binding( + parameter.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(declared_ty), + ); + } else { + let ty = if let Some(default_expr) = default_expr { + let default_ty = self.file_expression_type(default_expr); + UnionType::from_two_elements(db, Type::unknown(), default_ty) + } else if let Some(ty) = self.special_first_method_parameter_type(parameter) { + ty + } else { + Type::unknown() + }; + + self.add_binding(parameter.into(), definition) + .insert(self, ty); + } + } + + /// Set initial declared/inferred types for a `*args` variadic positional parameter. + /// + /// The annotated type is implicitly wrapped in a homogeneous tuple. + /// + /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. + /// + /// [`infer_parameter_definition`]: Self::infer_parameter_definition + pub(super) fn infer_variadic_positional_parameter_definition( + &mut self, + parameter: &'ast ast::Parameter, + definition: Definition<'db>, + ) { + let db = self.db(); + + if let Some(annotation) = parameter.annotation() { + let ty = if annotation.is_starred_expr() { + todo_type!("PEP 646") + } else { + let annotated_type = self.file_expression_type(annotation); + if let Type::TypeVar(typevar) = annotated_type + && typevar.is_paramspec(db) + { + match typevar.paramspec_attr(db) { + // `*args: P.args` + Some(ParamSpecAttrKind::Args) => annotated_type, + + // `*args: P.kwargs` + Some(ParamSpecAttrKind::Kwargs) => { + // TODO: Should this diagnostic be raised as part of + // `ArgumentTypeChecker`? + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, annotation) + { + let name = typevar.name(db); + let mut diag = builder.into_diagnostic(format_args!( + "`{name}.kwargs` is valid only in `**kwargs` annotation", + )); + diag.set_primary_message(format_args!( + "Did you mean `{name}.args`?" + )); + add_type_expression_reference_link(diag); + } + Type::homogeneous_tuple(db, Type::unknown()) + } + + // `*args: P` + None => { + // The diagnostic for this case is handled in `in_type_expression`. + Type::homogeneous_tuple(db, Type::unknown()) + } + } + } else { + Type::homogeneous_tuple(db, annotated_type) + } + }; + + self.add_declaration_with_binding( + parameter.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(ty), + ); + } else { + let inferred_ty = Type::homogeneous_tuple(db, Type::unknown()); + self.add_binding(parameter.into(), definition) + .insert(self, inferred_ty); + } + } + + /// Special case for unannotated `cls` and `self` arguments to class methods and instance methods. + fn special_first_method_parameter_type( + &mut self, + parameter: &ast::Parameter, + ) -> Option> { + let db = self.db(); + let file = self.file(); + + let function_scope_id = self.scope(); + let function_scope = function_scope_id.scope(db); + let function = function_scope.node().as_function()?; + + let parent_file_scope_id = function_scope.parent()?; + let mut parent_scope_id = parent_file_scope_id.to_scope_id(db, file); + + // Skip type parameter scopes, if the method itself is generic. + if parent_scope_id.is_annotation(db) { + let parent_scope = parent_scope_id.scope(db); + parent_scope_id = parent_scope.parent()?.to_scope_id(db, file); + } + + // Return early if this is not a method inside a class. + let class = parent_scope_id.scope(db).node().as_class()?; + + let method_definition = self.index.expect_single_definition(function); + let DefinitionKind::Function(function_definition) = method_definition.kind(db) else { + return None; + }; + + if function_definition + .node(self.module()) + .parameters + .index(parameter.name()) + .is_none_or(|index| index != 0) + { + return None; + } + + let function_node = function_definition.node(self.module()); + let function_name = &function_node.name; + + let mut is_classmethod = is_implicit_classmethod(function_name); + let inference = infer_definition_types(db, method_definition); + for decorator in &function_node.decorator_list { + let decorator_ty = inference.expression_type(&decorator.expression); + if let Some(known_class) = decorator_ty + .as_class_literal() + .and_then(|class| class.known(db)) + { + if known_class == KnownClass::Staticmethod { + return None; + } + + is_classmethod |= known_class == KnownClass::Classmethod; + } + } + + let class_definition = self.index.expect_single_definition(class); + let class_literal = infer_definition_types(db, class_definition) + .declaration_type(class_definition) + .inner_type() + .as_class_literal()?; + + let typing_self = typing_self(db, self.scope(), Some(method_definition), class_literal); + if is_classmethod || function_name == "__new__" { + typing_self + .map(|typing_self| SubclassOfType::from(db, SubclassOfInner::TypeVar(typing_self))) + } else { + typing_self.map(Type::TypeVar) + } + } + + /// Set initial declared/inferred types for a `**kwargs` keyword-variadic parameter. + /// + /// The annotated type is implicitly wrapped in a string-keyed dictionary. + /// + /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. + /// + /// [`infer_parameter_definition`]: Self::infer_parameter_definition + pub(super) fn infer_variadic_keyword_parameter_definition( + &mut self, + parameter: &'ast ast::Parameter, + definition: Definition<'db>, + ) { + let db = self.db(); + + if let Some(annotation) = parameter.annotation() { + let annotated_type = self.file_expression_type(annotation); + let ty = if let Type::TypeVar(typevar) = annotated_type + && typevar.is_paramspec(db) + { + match typevar.paramspec_attr(db) { + // `**kwargs: P.args` + Some(ParamSpecAttrKind::Args) => { + // TODO: Should this diagnostic be raised as part of `ArgumentTypeChecker`? + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, annotation) + { + let name = typevar.name(db); + let mut diag = builder.into_diagnostic(format_args!( + "`{name}.args` is valid only in `*args` annotation", + )); + diag.set_primary_message(format_args!("Did you mean `{name}.kwargs`?")); + add_type_expression_reference_link(diag); + } + KnownClass::Dict.to_specialized_instance( + db, + &[KnownClass::Str.to_instance(db), Type::unknown()], + ) + } + + // `**kwargs: P.kwargs` + Some(ParamSpecAttrKind::Kwargs) => annotated_type, + + // `**kwargs: P` + None => { + // The diagnostic for this case is handled in `in_type_expression`. + KnownClass::Dict.to_specialized_instance( + db, + &[KnownClass::Str.to_instance(db), Type::unknown()], + ) + } + } + } else { + KnownClass::Dict + .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), annotated_type]) + }; + self.add_declaration_with_binding( + parameter.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(ty), + ); + } else { + let inferred_ty = KnownClass::Dict + .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::unknown()]); + + self.add_binding(parameter.into(), definition) + .insert(self, inferred_ty); + } + } +} diff --git a/crates/ty_python_semantic/src/types/infer/builder/imports.rs b/crates/ty_python_semantic/src/types/infer/builder/imports.rs new file mode 100644 index 0000000000000..0bfef1d10a004 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/imports.rs @@ -0,0 +1,662 @@ +use ruff_python_ast as ast; +use ruff_text_size::{Ranged, TextRange}; +use ty_module_resolver::{ + ModuleName, ModuleNameResolutionError, ModuleResolveMode, resolve_module, search_paths, +}; + +use crate::{ + Program, TypeQualifiers, add_inferred_python_version_hint_to_diagnostic, + place::{DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin}, + semantic_index::definition::Definition, + types::{ + Type, TypeAndQualifiers, + diagnostic::{ + POSSIBLY_MISSING_IMPORT, UNRESOLVED_IMPORT, + hint_if_stdlib_attribute_exists_on_other_versions, + hint_if_stdlib_submodule_exists_on_other_versions, + }, + infer::{TypeInferenceBuilder, builder::DeclaredAndInferredType}, + infer_definition_types, + }, +}; + +impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { + pub(super) fn infer_import_statement(&mut self, import: &ast::StmtImport) { + let ast::StmtImport { + names, + is_lazy: _, + range: _, + node_index: _, + } = import; + + for alias in names { + self.infer_definition(alias); + } + } + + fn report_unresolved_import( + &self, + import_node: ast::AnyNodeRef<'_>, + range: TextRange, + level: u32, + module: Option<&str>, + module_name: Option<&ModuleName>, + ) { + let db = self.db(); + let is_import_reachable = self.is_reachable(import_node); + + if !is_import_reachable { + return; + } + + if let Some(module_name) = &module_name + && (self + .settings() + .allowed_unresolved_imports + .matches(module_name) + .is_include() + || self + .settings() + .replace_imports_with_any + .matches(module_name) + .is_include()) + { + return; + } + + let Some(builder) = self.context.report_lint(&UNRESOLVED_IMPORT, range) else { + return; + }; + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot resolve imported module `{}`", + format_import_from_module(level, module) + )); + + if level == 0 { + if let Some(module_name) = module_name { + let program = Program::get(db); + let typeshed_versions = program.search_paths(db).typeshed_versions(); + + // Loop over ancestors in case we have info on the parent module but not submodule + for module_name in module_name.ancestors() { + if let Some(version_range) = typeshed_versions.exact(&module_name) { + // We know it is a stdlib module on *some* Python versions... + let python_version = program.python_version(db); + if !version_range.contains(python_version) { + // ...But not on *this* Python version. + diagnostic.info(format_args!( + "The stdlib module `{module_name}` is only available on Python {version_range}", + version_range = version_range.diagnostic_display(), + )); + add_inferred_python_version_hint_to_diagnostic( + db, + &mut diagnostic, + "resolving modules", + ); + return; + } + // We found the most precise answer we could, stop searching + break; + } + } + } + } else { + if let Some(better_level) = (0..level).rev().find(|reduced_level| { + let Ok(module_name) = + ModuleName::from_identifier_parts(db, self.file(), module, *reduced_level) + else { + return false; + }; + resolve_module(db, self.file(), &module_name).is_some() + }) { + diagnostic + .help("The module can be resolved if the number of leading dots is reduced"); + diagnostic.help(format_args!( + "Did you mean `{}`?", + format_import_from_module(better_level, module) + )); + diagnostic.set_concise_message(format_args!( + "Cannot resolve imported module `{}` - did you mean `{}`?", + format_import_from_module(level, module), + format_import_from_module(better_level, module) + )); + } + } + + // Add search paths information to the diagnostic + // Use the same search paths function that is used in actual module resolution + let verbose = db.verbose(); + let search_paths = search_paths(db, ModuleResolveMode::StubsAllowed); + + diagnostic.info(format_args!( + "Searched in the following paths during module resolution:" + )); + + let mut search_paths = search_paths.enumerate().peekable(); + + while let Some((index, path)) = search_paths.next() { + if index > 4 && !verbose && search_paths.peek().is_some() { + let more = search_paths.count() + 1; + diagnostic.info(format_args!( + " ... and {more} more paths. Run with `-v` to see all paths." + )); + break; + } + diagnostic.info(format_args!( + " {}. {} ({})", + index + 1, + path, + path.describe_kind() + )); + } + + diagnostic.info( + "make sure your Python environment is properly configured: \ + https://docs.astral.sh/ty/modules/#python-environment", + ); + } + + pub(super) fn infer_import_definition( + &mut self, + node: &ast::StmtImport, + alias: &ast::Alias, + definition: Definition<'db>, + ) { + let ast::Alias { + range: _, + node_index: _, + name, + asname, + } = alias; + + // The name of the module being imported + let Some(full_module_name) = ModuleName::new(name) else { + tracing::debug!("Failed to resolve import due to invalid syntax"); + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + + if self + .settings() + .replace_imports_with_any + .matches(&full_module_name) + .is_include() + { + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(Type::any()), + ); + return; + } + + // Resolve the module being imported. + let Some(full_module_ty) = self.module_type_from_name(&full_module_name) else { + self.report_unresolved_import( + node.into(), + alias.range(), + 0, + Some(name), + Some(&full_module_name), + ); + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + + let binding_ty = if asname.is_some() { + // If we are renaming the imported module via an `as` clause, then we bind the resolved + // module's type to that name, even if that module is nested. + full_module_ty + } else if full_module_name.contains('.') { + // If there's no `as` clause and the imported module is nested, we're not going to bind + // the resolved module itself into the current scope; we're going to bind the top-most + // parent package of that module. + let topmost_parent_name = + ModuleName::new(full_module_name.components().next().unwrap()).unwrap(); + let Some(topmost_parent_ty) = self.module_type_from_name(&topmost_parent_name) else { + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + topmost_parent_ty + } else { + // If there's no `as` clause and the imported module isn't nested, then the imported + // module _is_ what we bind into the current scope. + full_module_ty + }; + + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(binding_ty), + ); + } + + pub(super) fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) { + let ast::StmtImportFrom { + module: _, + names, + level: _, + is_lazy: _, + range: _, + node_index: _, + } = import; + + let db = self.db(); + + self.check_import_from_module_is_resolvable(import); + + for alias in names { + for definition in self.index.definitions(alias) { + let inferred = infer_definition_types(db, *definition); + // Check non-star imports for deprecations + if definition.kind(db).as_star_import().is_none() { + // In the initial cycle, `declaration_types()` is empty, so no deprecation check is performed. + for ty in inferred.declaration_types() { + self.check_deprecated(alias, ty.inner); + } + } + self.extend_definition(inferred); + } + } + } + + /// Resolve the [`ModuleName`], and the type of the module, being referred to by an + /// [`ast::StmtImportFrom`] node. Emit a diagnostic if the module cannot be resolved. + fn check_import_from_module_is_resolvable(&mut self, import_from: &ast::StmtImportFrom) { + let ast::StmtImportFrom { module, level, .. } = import_from; + + let db = self.db(); + + // For diagnostics, we want to highlight the unresolvable + // module and not the entire `from ... import ...` statement. + let module_ref = module + .as_ref() + .map(ast::AnyNodeRef::from) + .unwrap_or_else(|| ast::AnyNodeRef::from(import_from)); + let module = module.as_deref(); + + tracing::trace!( + "Resolving import statement from module `{}` into file `{}`", + format_import_from_module(*level, module), + self.file().path(db), + ); + let module_name = ModuleName::from_import_statement(db, self.file(), import_from); + + let module_name = match module_name { + Ok(module_name) => module_name, + Err(ModuleNameResolutionError::InvalidSyntax) => { + tracing::debug!("Failed to resolve import due to invalid syntax"); + // Invalid syntax diagnostics are emitted elsewhere. + return; + } + Err(ModuleNameResolutionError::TooManyDots) => { + tracing::debug!( + "Relative module resolution `{}` failed: too many leading dots", + format_import_from_module(*level, module), + ); + self.report_unresolved_import( + import_from.into(), + module_ref.range(), + *level, + module, + None, + ); + return; + } + Err(ModuleNameResolutionError::UnknownCurrentModule) => { + tracing::debug!( + "Relative module resolution `{}` failed: could not resolve file `{}` to a module \ + (try adjusting configured search paths?)", + format_import_from_module(*level, module), + self.file().path(db) + ); + self.report_unresolved_import( + import_from.into(), + module_ref.range(), + *level, + module, + None, + ); + return; + } + }; + + if resolve_module(db, self.file(), &module_name).is_none() { + self.report_unresolved_import( + import_from.into(), + module_ref.range(), + *level, + module, + Some(&module_name), + ); + } + } + + pub(super) fn infer_import_from_definition( + &mut self, + import_from: &ast::StmtImportFrom, + alias: &ast::Alias, + definition: Definition<'db>, + ) { + let db = self.db(); + + let Ok(module_name) = ModuleName::from_import_statement(db, self.file(), import_from) + else { + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + + if self + .settings() + .replace_imports_with_any + .matches(&module_name) + .is_include() + { + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(Type::any()), + ); + return; + } + + let Some(module) = resolve_module(db, self.file(), &module_name) else { + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + + let module_ty = Type::module_literal(db, self.file(), module); + + let name = if let Some(star_import) = definition.kind(db).as_star_import() { + self.index + .place_table(self.scope().file_scope_id(db)) + .symbol(star_import.symbol_id()) + .name() + } else { + &alias.name.id + }; + + // Avoid looking up attributes on a module if a module imports from itself + // at the module-global scope, where the import definition itself is one of the + // bindings for the symbol being looked up, which would cause a query cycle. + // + // In nested scopes (e.g. function bodies), the module's global-scope definitions + // are resolved independently, so there is no cycle risk and the lookup is safe. + let skip_self_referential_member_lookup = module_ty + .as_module_literal() + .is_some_and(|module| Some(self.file()) == module.module(db).file(db)) + && self.scope().file_scope_id(db).is_global(); + + // Although it isn't the runtime semantics, we go to some trouble to prioritize a submodule + // over module `__getattr__`, because that's what other type checkers do. + let mut from_module_getattr = None; + + // First try loading the requested attribute from the module. + if !skip_self_referential_member_lookup { + if let PlaceAndQualifiers { + place: + Place::Defined(DefinedPlace { + ty, + definedness: boundness, + .. + }), + qualifiers, + } = module_ty.member(db, name) + { + if &alias.name != "*" && boundness == Definedness::PossiblyUndefined { + // TODO: Consider loading _both_ the attribute and any submodule and unioning them + // together if the attribute exists but is possibly-unbound. + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_MISSING_IMPORT, ast::AnyNodeRef::Alias(alias)) + { + builder.into_diagnostic(format_args!( + "Member `{name}` of module `{module_name}` may be missing", + )); + } + } + if qualifiers.contains(TypeQualifiers::FROM_MODULE_GETATTR) { + from_module_getattr = Some((ty, qualifiers)); + } else { + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::MightBeDifferent { + declared_ty: TypeAndQualifiers { + inner: ty, + origin: TypeOrigin::Declared, + qualifiers, + }, + inferred_ty: ty, + }, + ); + return; + } + } + } + + // Evaluate whether `X.Y` would constitute a valid submodule name, + // given a `from X import Y` statement. If it is valid, this will be `Some()`; + // else, it will be `None`. + let full_submodule_name = ModuleName::new(name).map(|final_part| { + let mut ret = module_name.clone(); + ret.extend(&final_part); + ret + }); + + // If the module doesn't bind the symbol, check if it's a submodule. This won't get + // handled by the `Type::member` call because it relies on the semantic index's + // `imported_modules` set. The semantic index does not include information about + // `from...import` statements because there are two things it cannot determine while only + // inspecting the content of the current file: + // + // - whether the imported symbol is an attribute or submodule + // - whether the containing file is in a module or a package (needed to correctly resolve + // relative imports) + // + // The first would be solvable by making it a _potentially_ imported modules set. The + // second is not. + // + // Regardless, for now, we sidestep all of that by repeating the submodule-or-attribute + // check here when inferring types for a `from...import` statement. + if let Some(submodule_type) = full_submodule_name + .as_ref() + .and_then(|submodule_name| self.module_type_from_name(submodule_name)) + { + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(submodule_type), + ); + return; + } + + // We've checked for a submodule, so now we can go ahead and use a type from module + // `__getattr__`. + if let Some((ty, qualifiers)) = from_module_getattr { + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::MightBeDifferent { + declared_ty: TypeAndQualifiers { + inner: ty, + origin: TypeOrigin::Declared, + qualifiers, + }, + inferred_ty: ty, + }, + ); + return; + } + + self.add_unknown_declaration_with_binding(alias.into(), definition); + + if &alias.name == "*" { + return; + } + + if !self.is_reachable(import_from) { + return; + } + + if self + .settings() + .allowed_unresolved_imports + .matches(full_submodule_name.as_ref().unwrap_or(&module_name)) + .is_include() + { + return; + } + + let Some(builder) = self + .context + .report_lint(&UNRESOLVED_IMPORT, ast::AnyNodeRef::Alias(alias)) + else { + return; + }; + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Module `{module_name}` has no member `{name}`" + )); + + let mut submodule_hint_added = false; + + if let Some(full_submodule_name) = full_submodule_name { + submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions( + db, + &mut diagnostic, + &full_submodule_name, + module, + ); + } + + if !submodule_hint_added { + hint_if_stdlib_attribute_exists_on_other_versions( + db, + diagnostic, + module_ty, + name, + "resolving imports", + ); + } + } + + /// Infer the implicit local definition `x = ` that + /// `from .x.y import z` or `from whatever.thispackage.x.y` can introduce in `__init__.py(i)`. + /// + /// For the definition `z`, see [`TypeInferenceBuilder::infer_import_from_definition`]. + /// + /// The runtime semantic of this kind of statement is to introduce a variable in the global + /// scope of this module *the first time it's imported in the entire program*. This + /// implementation just blindly introduces a local variable wherever the `from..import` is + /// (if the imports actually resolve). + /// + /// That gap between the semantics and implementation are currently the responsibility of the + /// code that actually creates these kinds of Definitions (so blindly introducing a local + /// is all we need to be doing here). + pub(super) fn infer_import_from_submodule_definition( + &mut self, + import_from: &'ast ast::StmtImportFrom, + definition: Definition<'db>, + ) { + let db = self.db(); + + // Get this package's absolute module name by resolving `.`, and make sure it exists + let Ok(thispackage_name) = ModuleName::package_for_file(db, self.file()) else { + self.add_binding(import_from.into(), definition) + .insert(self, Type::unknown()); + return; + }; + + let Some(module) = resolve_module(db, self.file(), &thispackage_name) else { + self.add_binding(import_from.into(), definition) + .insert(self, Type::unknown()); + return; + }; + + // We have `from whatever.thispackage.x.y ...` or `from .x.y ...` + // and we want to extract `x` (to ultimately construct `whatever.thispackage.x`): + + // First we normalize to `whatever.thispackage.x.y` + let Some(final_part) = ModuleName::from_identifier_parts( + db, + self.file(), + import_from.module.as_deref(), + import_from.level, + ) + .ok() + // `whatever.thispackage.x.y` => `x.y` + .and_then(|submodule_name| submodule_name.relative_to(&thispackage_name)) + // `x.y` => `x` + .and_then(|relative_submodule_name| { + relative_submodule_name + .components() + .next() + .and_then(ModuleName::new) + }) else { + self.add_binding(import_from.into(), definition) + .insert(self, Type::unknown()); + return; + }; + + // `x` => `whatever.thispackage.x` + let mut full_submodule_name = thispackage_name.clone(); + full_submodule_name.extend(&final_part); + + // Try to actually resolve the import `whatever.thispackage.x` + if let Some(submodule_type) = self.module_type_from_name(&full_submodule_name) { + // Success, introduce a binding! + // + // We explicitly don't introduce a *declaration* because it's actual ok + // (and fairly common) to overwrite this import with a function or class + // and we don't want it to be a type error to do so. + self.add_binding(import_from.into(), definition) + .insert(self, submodule_type); + return; + } + + // That didn't work, try to produce diagnostics + self.add_binding(import_from.into(), definition) + .insert(self, Type::unknown()); + + if self + .settings() + .allowed_unresolved_imports + .matches(&full_submodule_name) + .is_include() + { + return; + } + + if !self.is_reachable(import_from) { + return; + } + + let Some(builder) = self.context.report_lint( + &UNRESOLVED_IMPORT, + ast::AnyNodeRef::StmtImportFrom(import_from), + ) else { + return; + }; + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Module `{thispackage_name}` has no submodule `{final_part}`" + )); + + hint_if_stdlib_submodule_exists_on_other_versions( + db, + &mut diagnostic, + &full_submodule_name, + module, + ); + } +} + +fn format_import_from_module(level: u32, module: Option<&str>) -> String { + format!( + "{}{}", + ".".repeat(level as usize), + module.unwrap_or_default() + ) +} diff --git a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs new file mode 100644 index 0000000000000..c503461f8423f --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs @@ -0,0 +1,784 @@ +use crate::{ + Db, + semantic_index::definition::Definition, + types::{ + ClassLiteral, IntersectionType, KnownClass, KnownInstanceType, SpecialFormType, Type, + TypeContext, UnionType, + class::{ + DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, NamedTupleField, NamedTupleSpec, + }, + diagnostic::{ + INVALID_ARGUMENT_TYPE, INVALID_NAMED_TUPLE, MISSING_ARGUMENT, + PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, + }, + function::KnownFunction, + infer::TypeInferenceBuilder, + }, +}; +use ruff_python_ast::{self as ast, name::Name}; +use ruff_python_stdlib::{identifiers::is_identifier, keyword::is_keyword}; +use rustc_hash::FxHashSet; + +impl<'db> TypeInferenceBuilder<'db, '_> { + /// Infer a `typing.NamedTuple(typename, fields)` or `collections.namedtuple(typename, field_names)` call. + /// + /// This method *does not* call `infer_expression` on the object being called; + /// it is assumed that the type for this AST node has already been inferred before this method is called. + pub(super) fn infer_namedtuple_call_expression( + &mut self, + call_expr: &ast::ExprCall, + definition: Option>, + kind: NamedTupleKind, + ) -> Type<'db> { + let db = self.db(); + + // The fallback type reflects the fact that if the call were successful, + // it would return a class that: + // + // - Would be a subclass of `tuple[Unknown, ...]` + // - Would have all the generated methods included on the `NamedTupleLike` protocol + // - Would have a constructor method that would accept an unknown set of positional + // and keyword arguments + let fallback = || { + IntersectionType::from_elements( + db, + [ + Type::homogeneous_tuple(db, Type::unknown()).to_meta_type(db), + KnownClass::NamedTupleLike.to_subclass_of(db), + Type::unknown(), + ], + ) + }; + + let ast::Arguments { + args, + keywords, + range: _, + node_index: _, + } = &call_expr.arguments; + + // Check for variadic arguments early, before extracting positional args. + let has_starred = args.iter().any(ast::Expr::is_starred_expr); + let has_double_starred = keywords.iter().any(|kw| kw.arg.is_none()); + + // Emit diagnostic for missing required arguments or unsupported variadic arguments. + // For `typing.NamedTuple`, emit a diagnostic since variadic arguments are not supported. + // For `collections.namedtuple`, silently fall back since it's more permissive at runtime. + if (has_starred || has_double_starred) + && kind.is_typing() + && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, call_expr) + { + let arg_type = if has_starred && has_double_starred { + "Variadic positional and keyword arguments are" + } else if has_starred { + "Variadic positional arguments are" + } else { + "Variadic keyword arguments are" + }; + builder.into_diagnostic(format_args!( + "{arg_type} not supported in `NamedTuple()` calls" + )); + } + + // Extract typename and fields from positional or keyword arguments. + // For `collections.namedtuple`, both `typename` and `field_names` can be keyword arguments. + // For `typing.NamedTuple`, only positional arguments are supported. + let (name_arg, fields_arg, rest, name_from_keyword, fields_from_keyword): ( + Option<&ast::Expr>, + Option<&ast::Expr>, + &[ast::Expr], + bool, + bool, + ) = match kind { + NamedTupleKind::Collections => { + let typename_kw = call_expr.arguments.find_keyword("typename"); + let field_names_kw = call_expr.arguments.find_keyword("field_names"); + + match &**args { + [name, fields, rest @ ..] => (Some(name), Some(fields), rest, false, false), + [name, rest @ ..] => ( + Some(name), + field_names_kw.map(|kw| &kw.value), + rest, + false, + field_names_kw.is_some(), + ), + [] => ( + typename_kw.map(|kw| &kw.value), + field_names_kw.map(|kw| &kw.value), + &[], + typename_kw.is_some(), + field_names_kw.is_some(), + ), + } + } + NamedTupleKind::Typing => match &**args { + [name, fields, rest @ ..] => (Some(name), Some(fields), rest, false, false), + [name, rest @ ..] => (Some(name), None, rest, false, false), + [] => (None, None, &[], false, false), + }, + }; + + // Check if we have both required arguments. + let (Some(name_arg), Some(fields_arg)) = (name_arg, fields_arg) else { + for arg in args { + self.infer_expression(arg, TypeContext::default()); + } + for kw in keywords { + self.infer_expression(&kw.value, TypeContext::default()); + } + + if !has_starred && !has_double_starred { + let fields_param_name = match kind { + NamedTupleKind::Typing => "fields", + NamedTupleKind::Collections => "field_names", + }; + let missing = match (name_arg.is_none(), fields_arg.is_none()) { + (true, true) => format!("`typename` and `{fields_param_name}`"), + (true, false) => "`typename`".to_string(), + (false, true) => format!("`{fields_param_name}`"), + (false, false) => unreachable!(), + }; + let plural = name_arg.is_none() && fields_arg.is_none(); + if let Some(builder) = self.context.report_lint(&MISSING_ARGUMENT, call_expr) { + builder.into_diagnostic(format_args!( + "Missing required argument{} {missing} to `{kind}()`", + if plural { "s" } else { "" } + )); + } + } + return fallback(); + }; + + let name_type = self.infer_expression(name_arg, TypeContext::default()); + + for arg in rest { + self.infer_expression(arg, TypeContext::default()); + } + + // If any argument is a starred expression or any keyword is a double-starred expression, + // we can't statically determine the arguments, so fall back to normal call binding. + if has_starred || has_double_starred { + for kw in keywords { + self.infer_expression(&kw.value, TypeContext::default()); + } + return fallback(); + } + + // Check for excess positional arguments (only `typename` and `fields` are expected). + if !rest.is_empty() { + if let Some(builder) = self + .context + .report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, &rest[0]) + { + builder.into_diagnostic(format_args!( + "Too many positional arguments to function `{kind}`: expected 2, got {}", + args.len() + )); + } + } + + // Infer keyword arguments. + let mut default_types: Vec> = vec![]; + let mut defaults_kw: Option<&ast::Keyword> = None; + let mut rename_type = None; + + for kw in keywords { + // `kw.arg` is `None` for double-starred kwargs (`**kwargs`), but we already + // returned early above if there were any, so this should always be `Some`. + let arg = kw + .arg + .as_ref() + .expect("double-starred kwargs should have been handled above"); + + // Skip keywords that were used for the required arguments (already inferred above). + // These flags are only true for `collections.namedtuple`. + if name_from_keyword && arg.id.as_str() == "typename" { + continue; + } + if fields_from_keyword && arg.id.as_str() == "field_names" { + continue; + } + + let kw_type = self.infer_expression(&kw.value, TypeContext::default()); + + match arg.id.as_str() { + "defaults" if kind.is_collections() => { + defaults_kw = Some(kw); + // Extract element types from AST literals (using already-inferred types) + // or fall back to the inferred tuple spec. + match &kw.value { + ast::Expr::List(list) => { + // Elements were already inferred when we inferred kw.value above. + default_types = list + .elts + .iter() + .map(|elt| self.expression_type(elt)) + .collect(); + } + ast::Expr::Tuple(tuple) => { + // Elements were already inferred when we inferred kw.value above. + default_types = tuple + .elts + .iter() + .map(|elt| self.expression_type(elt)) + .collect(); + } + _ => { + // Fall back to using the already-inferred type. + // Try to extract element types from tuple. + if let Some(spec) = kw_type.exact_tuple_instance_spec(db) + && let Some(fixed) = spec.as_fixed_length() + { + default_types = fixed.all_elements().to_vec(); + } else { + // Can't determine individual types; use Any for each element. + let count = kw_type + .exact_tuple_instance_spec(db) + .and_then(|spec| spec.len().maximum()) + .unwrap_or(0); + default_types = vec![Type::any(); count]; + } + } + } + // Emit diagnostic for invalid types (not Iterable[Any] | None). + let iterable_any = + KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); + let valid_type = UnionType::from_two_elements(db, iterable_any, Type::none(db)); + if !kw_type.is_assignable_to(db, valid_type) + && let Some(builder) = + self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter `defaults` of `namedtuple()`" + )); + diagnostic.set_primary_message(format_args!( + "Expected `Iterable[Any] | None`, found `{}`", + kw_type.display(db) + )); + } + } + "rename" if kind.is_collections() => { + rename_type = Some(kw_type); + + // Emit diagnostic for non-bool types. + if !kw_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) + && let Some(builder) = + self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter `rename` of `namedtuple()`" + )); + diagnostic.set_primary_message(format_args!( + "Expected `bool`, found `{}`", + kw_type.display(db) + )); + } + } + "module" if kind.is_collections() => { + // Emit diagnostic for invalid types (not str | None). + let valid_type = UnionType::from_two_elements( + db, + KnownClass::Str.to_instance(db), + Type::none(db), + ); + if !kw_type.is_assignable_to(db, valid_type) + && let Some(builder) = + self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter `module` of `namedtuple()`" + )); + diagnostic.set_primary_message(format_args!( + "Expected `str | None`, found `{}`", + kw_type.display(db) + )); + } + } + // `typename` is valid as a keyword argument only for `collections.namedtuple`. + // If it was already provided positionally, emit an error. + "typename" if kind.is_collections() => { + if !args.is_empty() { + if let Some(builder) = + self.context.report_lint(&PARAMETER_ALREADY_ASSIGNED, kw) + { + builder.into_diagnostic(format_args!( + "Multiple values provided for parameter `typename` of `{kind}`" + )); + } + } + } + // `field_names` is valid only for `collections.namedtuple`. + // If it was already provided positionally, emit an error. + "field_names" if kind.is_collections() => { + if args.len() >= 2 { + if let Some(builder) = + self.context.report_lint(&PARAMETER_ALREADY_ASSIGNED, kw) + { + builder.into_diagnostic(format_args!( + "Multiple values provided for parameter `field_names` of `{kind}`" + )); + } + } + } + unknown_kwarg => { + // Report unknown keyword argument. + if let Some(builder) = self.context.report_lint(&UNKNOWN_ARGUMENT, kw) { + builder.into_diagnostic(format_args!( + "Argument `{unknown_kwarg}` does not match any known parameter of function `{kind}`", + )); + } + } + } + } + + // Extract name. + let name = if let Some(literal) = name_type.as_string_literal() { + Name::new(literal.value(db)) + } else { + // Name is not a string literal; use like we do for type() calls. + if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter `typename` of `{kind}()`" + )); + diagnostic.set_primary_message(format_args!( + "Expected `str`, found `{}`", + name_type.display(db) + )); + } + Name::new_static("") + }; + + // Handle fields based on which namedtuple variant. + let anchor = match definition { + Some(definition) => match kind { + NamedTupleKind::Collections => { + let spec = self.infer_collections_namedtuple_fields( + rename_type, + fields_arg, + &default_types, + defaults_kw, + ); + DynamicNamedTupleAnchor::CollectionsDefinition { definition, spec } + } + NamedTupleKind::Typing => { + // The `fields` argument to `typing.NamedTuple` cannot be inferred + // eagerly if it's not a dangling call, as it may contain forward references + // or recursive references. + self.deferred.insert(definition, self.multi_inference_state); + DynamicNamedTupleAnchor::TypingDefinition(definition) + } + }, + None => { + let call_node_index = call_expr.node_index.load(); + let scope = self.scope(); + let scope_anchor = scope + .node(db) + .node_index() + .unwrap_or(ast::NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("scope anchor should not be NodeIndex::NONE"); + let call_u32 = call_node_index + .as_u32() + .expect("call node should not be NodeIndex::NONE"); + let spec = match kind { + NamedTupleKind::Collections => self.infer_collections_namedtuple_fields( + rename_type, + fields_arg, + &default_types, + defaults_kw, + ), + NamedTupleKind::Typing => self.infer_typing_namedtuple_fields(fields_arg), + }; + DynamicNamedTupleAnchor::ScopeOffset { + scope, + offset: call_u32 - anchor_u32, + spec, + } + } + }; + + let namedtuple = DynamicNamedTupleLiteral::new(db, name, anchor); + + Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) + } + + fn infer_collections_namedtuple_fields( + &mut self, + rename_type: Option>, + fields_arg: &ast::Expr, + default_types: &[Type<'db>], + defaults_kw: Option<&ast::Keyword>, + ) -> NamedTupleSpec<'db> { + let db = self.db(); + + // `collections.namedtuple`: `field_names` is a list or tuple of strings, or a space or + // comma-separated string. + + // Check for `rename=True`. Use `is_always_true()` to handle truthy values + // (e.g., `rename=1`), though we'd still want a diagnostic for non-bool types. + let rename = rename_type.is_some_and(|ty| ty.bool(db).is_always_true()); + + let fields_type = self.infer_expression(fields_arg, TypeContext::default()); + + // Extract field names, first from the inferred type, then from the AST. + let maybe_field_names: Option> = + if let Some(string_literal) = fields_type.as_string_literal() { + // Handle space/comma-separated string. + Some( + string_literal + .value(db) + .replace(',', " ") + .split_whitespace() + .map(Name::new) + .collect(), + ) + } else if let Some(tuple_spec) = fields_type.tuple_instance_spec(db) + && let Some(fixed_tuple) = tuple_spec.as_fixed_length() + { + // Handle list/tuple of strings (must be fixed-length). + fixed_tuple + .all_elements() + .iter() + .map(|elt| elt.as_string_literal().map(|s| Name::new(s.value(db)))) + .collect() + } else { + // Get the elements from the list or tuple literal. + let elements = match fields_arg { + ast::Expr::List(list) => Some(&list.elts), + ast::Expr::Tuple(tuple) => Some(&tuple.elts), + _ => None, + }; + + elements.and_then(|elts| { + elts.iter() + .map(|elt| { + // Each element should be a string literal. + let field_ty = self.expression_type(elt); + let field_lit = field_ty.as_string_literal()?; + Some(Name::new(field_lit.value(db))) + }) + .collect::>() + }) + }; + + if maybe_field_names.is_none() { + // Emit diagnostic if the type is outright invalid (not str | Iterable[str]). + let iterable_str = KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); + let valid_type = + UnionType::from_two_elements(db, KnownClass::Str.to_instance(db), iterable_str); + if !fields_type.is_assignable_to(db, valid_type) + && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, fields_arg) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter `field_names` of `namedtuple()`" + )); + diagnostic.set_primary_message(format_args!( + "Expected `str` or an iterable of strings, found `{}`", + fields_type.display(db) + )); + } + } + + let Some(mut field_names) = maybe_field_names else { + // Couldn't determine fields statically; attribute lookups will return Any. + return NamedTupleSpec::unknown(db); + }; + + // When `rename` is false (or not specified), emit diagnostics for invalid + // field names. These all raise ValueError at runtime. When `rename=True`, + // invalid names are automatically replaced with `_0`, `_1`, etc., so no + // diagnostic is needed. + if !rename { + self.check_invalid_namedtuple_field_names( + &field_names, + fields_arg, + NamedTupleKind::Collections, + ); + } else { + // Apply rename logic. + let mut seen_names = FxHashSet::<&str>::default(); + for (i, field_name) in field_names.iter_mut().enumerate() { + let name_str = field_name.as_str(); + let needs_rename = name_str.starts_with('_') + || is_keyword(name_str) + || !is_identifier(name_str) + || seen_names.contains(name_str); + if needs_rename { + *field_name = Name::new(format!("_{i}")); + } + seen_names.insert(field_name.as_str()); + } + } + + let num_fields = field_names.len(); + let defaults_count = default_types.len(); + + if defaults_count > num_fields + && let Some(defaults_kw) = defaults_kw + && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, defaults_kw) + { + let mut diagnostic = + builder.into_diagnostic(format_args!("Too many defaults for `namedtuple()`")); + diagnostic.set_primary_message(format_args!( + "Got {defaults_count} default values but only {num_fields} field names" + )); + diagnostic.info("This will raise `TypeError` at runtime"); + } + + let defaults_count = defaults_count.min(num_fields); + let fields = field_names + .iter() + .enumerate() + .map(|(i, field_name)| { + let default = if defaults_count > 0 && i >= num_fields - defaults_count { + // Index into default_types: first default corresponds to first + // field that has a default. + let default_idx = i - (num_fields - defaults_count); + Some(default_types[default_idx]) + } else { + None + }; + NamedTupleField { + name: field_name.clone(), + ty: Type::any(), + default, + } + }) + .collect(); + + NamedTupleSpec::known(db, fields) + } + + pub(super) fn infer_typing_namedtuple_fields( + &mut self, + fields_arg: &ast::Expr, + ) -> NamedTupleSpec<'db> { + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + enum SequenceKind { + List, + Tuple, + } + + let db = self.db(); + + // Get the elements from the list or tuple literal. + let (elements, field_arg_kind) = match fields_arg { + ast::Expr::List(list) => (&list.elts, SequenceKind::List), + ast::Expr::Tuple(tuple) => (&tuple.elts, SequenceKind::Tuple), + _ => { + self.infer_expression(fields_arg, TypeContext::default()); + if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { + let mut diagnostic = builder.into_diagnostic( + "Invalid argument to parameter `fields` of `NamedTuple()`", + ); + diagnostic.set_primary_message("`fields` must be a literal list or tuple"); + } + return NamedTupleSpec::unknown(db); + } + }; + + let mut fields = vec![]; + + for (i, element) in elements.iter().enumerate() { + // Each element should be a tuple or list like ("field_name", type) or ["field_name", type]. + let (field_spec_elts, field_spec_kind) = match element { + ast::Expr::Tuple(tuple) => (&tuple.elts, SequenceKind::Tuple), + ast::Expr::List(list) => (&list.elts, SequenceKind::List), + _ => { + self.infer_expression(element, TypeContext::default()); + for element in &elements[(i + 1)..] { + self.infer_expression(element, TypeContext::default()); + } + match field_arg_kind { + SequenceKind::List => { + self.store_expression_type( + fields_arg, + KnownClass::List.to_instance(db), + ); + } + SequenceKind::Tuple => self.store_expression_type( + fields_arg, + Type::homogeneous_tuple(db, Type::unknown()), + ), + } + if let Some(builder) = + self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) + { + let mut diagnostic = builder.into_diagnostic( + "Invalid argument to parameter `fields` of `NamedTuple()`", + ); + diagnostic.set_primary_message( + "`fields` must be a sequence of literal lists or tuples", + ); + } + return NamedTupleSpec::unknown(db); + } + }; + + let [name_expr, declaration_expr] = &**field_spec_elts else { + self.infer_expression(element, TypeContext::default()); + for element in &elements[(i + 1)..] { + self.infer_expression(element, TypeContext::default()); + } + match field_arg_kind { + SequenceKind::List => { + self.store_expression_type(fields_arg, KnownClass::List.to_instance(db)); + } + SequenceKind::Tuple => self.store_expression_type( + fields_arg, + Type::homogeneous_tuple(db, Type::unknown()), + ), + } + if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { + let mut diagnostic = builder.into_diagnostic( + "Invalid argument to parameter `fields` of `NamedTuple()`", + ); + diagnostic.set_primary_message( + "Each element in `fields` must be a length-2 tuple or list", + ); + } + return NamedTupleSpec::unknown(db); + }; + + let name_type = self.infer_expression(name_expr, TypeContext::default()); + let declared_type = self.infer_type_expression(declaration_expr); + + let element_type = match field_spec_kind { + SequenceKind::Tuple => Type::heterogeneous_tuple(db, [name_type, declared_type]), + SequenceKind::List => KnownClass::List.to_specialized_instance( + db, + &[UnionType::from_two_elements(db, name_type, declared_type)], + ), + }; + + self.store_expression_type(element, element_type); + + let Some(name) = name_type.as_string_literal() else { + for element in &elements[(i + 1)..] { + self.infer_expression(element, TypeContext::default()); + } + match field_arg_kind { + SequenceKind::List => { + self.store_expression_type(fields_arg, KnownClass::List.to_instance(db)); + } + SequenceKind::Tuple => self.store_expression_type( + fields_arg, + Type::homogeneous_tuple(db, Type::unknown()), + ), + } + if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, name_expr) { + let mut diagnostic = + builder.into_diagnostic("Invalid `NamedTuple` field name definition"); + diagnostic.set_primary_message(format_args!( + "Expected a string literal for the field name, found `{}`", + name_type.display(db) + )); + } + return NamedTupleSpec::unknown(db); + }; + + let field = NamedTupleField { + name: Name::new(name.value(db)), + ty: declared_type, + default: None, + }; + + fields.push(field); + } + + let names: Vec = fields.iter().map(|f| f.name.clone()).collect(); + + self.check_invalid_namedtuple_field_names(&names, fields_arg, NamedTupleKind::Typing); + + let spec = NamedTupleSpec::known(db, fields.into_boxed_slice()); + self.store_expression_type( + fields_arg, + Type::KnownInstance(KnownInstanceType::NamedTupleSpec(spec)), + ); + spec + } + + /// Report diagnostics for invalid field names in a namedtuple definition. + fn check_invalid_namedtuple_field_names( + &self, + field_names: &[Name], + fields_arg: &ast::Expr, + kind: NamedTupleKind, + ) { + for (i, field_name) in field_names.iter().enumerate() { + // Check for duplicate field names. + if field_names[..i].iter().any(|f| f == field_name) + && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Duplicate field name `{field_name}` in `{kind}()`" + )); + diagnostic.set_primary_message(format_args!( + "Field `{field_name}` already defined; will raise `ValueError` at runtime" + )); + } + + if field_name.starts_with('_') + && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Field name `{field_name}` in `{kind}()` cannot start with an underscore" + )); + diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + } else if is_keyword(field_name) + && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Field name `{field_name}` in `{kind}()` cannot be a Python keyword" + )); + diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + } else if !is_identifier(field_name) + && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Field name `{field_name}` in `{kind}()` is not a valid identifier" + )); + diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum NamedTupleKind { + Collections, + Typing, +} + +impl NamedTupleKind { + const fn is_collections(self) -> bool { + matches!(self, Self::Collections) + } + + const fn is_typing(self) -> bool { + matches!(self, Self::Typing) + } + + pub(super) fn from_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { + match ty { + Type::SpecialForm(SpecialFormType::NamedTuple) => Some(NamedTupleKind::Typing), + Type::FunctionLiteral(function) => function + .is_known(db, KnownFunction::NamedTuple) + .then_some(NamedTupleKind::Collections), + _ => None, + } + } +} + +impl std::fmt::Display for NamedTupleKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + NamedTupleKind::Collections => "namedtuple", + NamedTupleKind::Typing => "NamedTuple", + }) + } +} diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index d4ba2ac0084a7..faa24be3f93fb 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -1,8 +1,9 @@ -use itertools::{EitherOrBoth, Itertools}; +use itertools::{Either, EitherOrBoth, Itertools}; use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; use ruff_db::parsed::parsed_module; use ruff_python_ast::{self as ast, ExprContext}; use ruff_text_size::Ranged; +use ty_module_resolver::file_to_module; use super::TypeInferenceBuilder; use crate::place::{DefinedPlace, Definedness, Place}; @@ -10,25 +11,31 @@ use crate::semantic_index::SemanticIndex; use crate::semantic_index::definition::Definition; use crate::semantic_index::place::{PlaceExpr, PlaceExprRef}; use crate::semantic_index::scope::FileScopeId; +use crate::types::call::CallErrorKind; use crate::types::call::bind::CallableDescription; use crate::types::constraints::ConstraintSetBuilder; use crate::types::diagnostic::{ - INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, - report_invalid_arguments_to_annotated, + CALL_NON_CALLABLE, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_KEY, + INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, + TypedDictDeleteErrorKind, report_cannot_delete_typed_dict_key, + report_invalid_arguments_to_annotated, report_not_subscriptable, }; use crate::types::generics::{GenericContext, InferableTypeVars, bind_typevar}; use crate::types::infer::InferenceFlags; +use crate::types::infer::builder::{ArgExpr, ArgumentsIter}; use crate::types::special_form::AliasSpec; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleType}; +use crate::types::typed_dict::{TypedDictAssignmentKind, TypedDictKeyAssignment}; use crate::types::{ - BoundTypeVarInstance, CallableType, DynamicType, InternedType, KnownClass, KnownInstanceType, - Parameters, SpecialFormType, StaticClassLiteral, Type, TypeAliasType, TypeContext, - TypeVarBoundOrConstraints, UnionType, UnionTypeInstance, any_over_type, todo_type, + BoundTypeVarInstance, CallArguments, CallDunderError, CallableType, DynamicType, InternedType, + KnownClass, KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, + StaticClassLiteral, Type, TypeAliasType, TypeContext, TypeVarBoundOrConstraints, UnionType, + UnionTypeInstance, any_over_type, todo_type, }; use crate::{Db, FxOrderSet}; -impl<'db> TypeInferenceBuilder<'db, '_> { +impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { pub(super) fn infer_subscript_expression( &mut self, subscript: &ast::ExprSubscript, @@ -823,6 +830,174 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } + /// Infer the type of the expression that represents an explicit specialization of a + /// `ParamSpec` type variable. + fn infer_paramspec_explicit_specialization_value( + &mut self, + expr: &ast::Expr, + exactly_one_paramspec: bool, + ) -> Result, ()> { + let db = self.db(); + + match expr { + ast::Expr::EllipsisLiteral(_) => { + return Ok(Type::paramspec_value_callable( + db, + Parameters::gradual_form(), + )); + } + + ast::Expr::Tuple(_) if !exactly_one_paramspec => { + // Tuple expression is only allowed when the generic context contains only one + // `ParamSpec` type variable and no other type variables. + } + + ast::Expr::Tuple(ast::ExprTuple { elts, .. }) + | ast::Expr::List(ast::ExprList { elts, .. }) => { + let mut parameter_types = Vec::with_capacity(elts.len()); + + // Whether to infer `Todo` for the parameters + let mut return_todo = false; + + for param in elts { + let param_type = self.infer_type_expression(param); + // This is similar to what we currently do for inferring tuple type expression. + // We currently infer `Todo` for the parameters to avoid invalid diagnostics + // when trying to check for assignability or any other relation. For example, + // `*tuple[int, str]`, `Unpack[]`, etc. are not yet supported. + return_todo |= param_type.is_todo() + && matches!(param, ast::Expr::Starred(_) | ast::Expr::Subscript(_)); + parameter_types.push(param_type); + } + + let parameters = if return_todo { + // TODO: `Unpack` + Parameters::todo() + } else { + Parameters::new( + self.db(), + parameter_types.iter().map(|param_type| { + Parameter::positional_only(None).with_annotated_type(*param_type) + }), + ) + }; + + return Ok(Type::paramspec_value_callable(db, parameters)); + } + + ast::Expr::Subscript(_) => { + // TODO: Support `Concatenate[...]` + return Ok(Type::paramspec_value_callable(db, Parameters::todo())); + } + + ast::Expr::Name(name) => { + if name.is_invalid() { + return Err(()); + } + + let param_type = self.infer_type_expression(expr); + + match param_type { + Type::TypeVar(typevar) if typevar.is_paramspec(db) => { + return Ok(param_type); + } + + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) + if typevar.is_paramspec(db) => + { + if let Some(diagnostic_builder) = + self.context.report_lint(&INVALID_TYPE_ARGUMENTS, expr) + { + diagnostic_builder.into_diagnostic(format_args!( + "ParamSpec `{}` is unbound", + typevar.name(self.db()) + )); + } + return Err(()); + } + + // This is to handle the following case: + // + // ```python + // from typing import ParamSpec + // + // class Foo[**P]: ... + // + // Foo[ParamSpec] # P: (ParamSpec, /) + // ``` + Type::NominalInstance(nominal) + if nominal.has_known_class(self.db(), KnownClass::ParamSpec) => + { + return Ok(Type::paramspec_value_callable( + db, + Parameters::new( + self.db(), + [ + Parameter::positional_only(None) + .with_annotated_type(param_type), + ], + ), + )); + } + + _ if exactly_one_paramspec => { + // Square brackets are optional when `ParamSpec` is the only type variable + // being specialized. This means that a single name expression represents a + // parameter list with a single parameter. For example, + // + // ```python + // class OnlyParamSpec[**P]: ... + // + // OnlyParamSpec[int] # P: (int, /) + // ``` + let parameters = + if param_type.is_todo() { + Parameters::todo() + } else { + Parameters::new( + self.db(), + [Parameter::positional_only(None) + .with_annotated_type(param_type)], + ) + }; + return Ok(Type::paramspec_value_callable(db, parameters)); + } + + // This is specifically to handle a case where there are more than one type + // variables and at least one of them is a `ParamSpec` which is specialized + // using `typing.Any`. This isn't explicitly allowed in the spec, but both mypy + // and Pyright allows this and the ecosystem report suggested there are usages + // of this in the wild e.g., `staticmethod[Any, Any]`. For example, + // + // ```python + // class Foo[**P, T]: ... + // + // Foo[Any, int] # P: (Any, /), T: int + // ``` + Type::Dynamic(DynamicType::Any) => { + return Ok(Type::paramspec_value_callable( + db, + Parameters::gradual_form(), + )); + } + + _ => {} + } + } + + _ => {} + } + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, expr) { + builder.into_diagnostic( + "Type argument for `ParamSpec` must be either \ + a list of types, `ParamSpec`, `Concatenate`, or `...`", + ); + } + + Err(()) + } + pub(super) fn infer_subscript_expression_types( &self, subscript: &ast::ExprSubscript, @@ -918,6 +1093,600 @@ impl<'db> TypeInferenceBuilder<'db, '_> { _ => KnownClass::Slice.to_instance(db), } } + + /// Validate a subscript assignment of the form `object[key] = rhs_value`. + pub(super) fn validate_subscript_assignment( + &mut self, + target: &ast::ExprSubscript, + rhs_value: &ast::Expr, + infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, + ) -> bool { + let ast::ExprSubscript { + range: _, + node_index: _, + value: object, + slice, + ctx: _, + } = target; + + let object_ty = self.infer_expression(object, TypeContext::default()); + let mut infer_slice_ty = |builder: &mut Self, tcx| builder.infer_expression(slice, tcx); + + self.validate_subscript_assignment_impl( + target, + None, + object_ty, + &mut infer_slice_ty, + rhs_value, + infer_rhs_value, + true, + ) + } + + #[expect(clippy::too_many_arguments)] + fn validate_subscript_assignment_impl( + &mut self, + target: &ast::ExprSubscript, + full_object_ty: Option>, + object_ty: Type<'db>, + infer_slice_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, + rhs_value_node: &ast::Expr, + infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, + emit_diagnostic: bool, + ) -> bool { + /// Given a string literal or a union of string literals, return an iterator over the contained + /// strings, or `None`, if the type is neither. + fn key_literals<'db>( + db: &'db dyn Db, + slice_ty: Type<'db>, + ) -> Option + 'db> { + if let Some(literal) = slice_ty.as_string_literal() { + Some(Either::Left(std::iter::once(literal.value(db)))) + } else { + slice_ty.as_union().map(|union| { + Either::Right( + union + .elements(db) + .iter() + .filter_map(|ty| ty.as_string_literal().map(|lit| lit.value(db))), + ) + }) + } + } + + let db = self.db(); + + let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { + if let Some(full_object_ty) = full_object_ty { + diagnostic.info(format_args!( + "The full type of the subscripted object is `{}`", + full_object_ty.display(db) + )); + } + }; + + match object_ty { + Type::Union(union) => { + // TODO: Perform multi-inference here. + let slice_ty = infer_slice_ty(self, TypeContext::default()); + let rhs_value_ty = infer_rhs_value(self, TypeContext::default()); + + // Note that we use a loop here instead of .all(…) to avoid short-circuiting. + // We need to keep iterating to emit all diagnostics. + let mut valid = true; + for element_ty in union.elements(db) { + valid &= self.validate_subscript_assignment_impl( + target, + full_object_ty.or(Some(object_ty)), + *element_ty, + &mut |_, _| slice_ty, + rhs_value_node, + &mut |_, _| rhs_value_ty, + emit_diagnostic, + ); + } + valid + } + + Type::Intersection(intersection) => { + // TODO: Perform multi-inference here. + let slice_ty = infer_slice_ty(self, TypeContext::default()); + let rhs_value_ty = infer_rhs_value(self, TypeContext::default()); + + let mut check_positive_elements = |emit_diagnostic_and_short_circuit| { + let mut valid = false; + for element_ty in intersection.positive(db) { + valid |= self.validate_subscript_assignment_impl( + target, + full_object_ty.or(Some(object_ty)), + *element_ty, + &mut |_, _| slice_ty, + rhs_value_node, + &mut |_, _| rhs_value_ty, + emit_diagnostic_and_short_circuit, + ); + + if !valid && emit_diagnostic_and_short_circuit { + break; + } + } + + valid + }; + + // Perform an initial check of all elements. If the assignment is valid + // for at least one element, we do not emit any diagnostics. Otherwise, + // we re-run the check and emit a diagnostic on the first failing element. + let valid = check_positive_elements(false); + + if !valid { + check_positive_elements(true); + } + + valid + } + + Type::TypedDict(typed_dict) => { + // As an optimization, prevent calling `__setitem__` on (unions of) large `TypedDict`s, and + // validate the assignment ourselves. This also allows us to emit better diagnostics. + + // TODO: Use type context here. + let slice_ty = infer_slice_ty(self, TypeContext::default()); + let rhs_value_ty = infer_rhs_value(self, TypeContext::default()); + + let mut valid = true; + let Some(keys) = key_literals(db, slice_ty) else { + // Check if the key has a valid type. We only allow string literals, a union of string literals, + // or a dynamic type like `Any`. We can do this by checking assignability to `LiteralString`, + // but we need to exclude `LiteralString` itself. This check would technically allow weird key + // types like `LiteralString & Any` to pass, but it does not need to be perfect. We would just + // fail to provide the "can only be subscripted with a string literal key" hint in that case. + + if slice_ty.is_dynamic() { + return true; + } + + let assigned_d = rhs_value_ty.display(db); + let value_d = object_ty.display(db); + + if slice_ty.is_assignable_to(db, Type::literal_string()) + && !slice_ty.is_equivalent_to(db, Type::literal_string()) + { + if let Some(builder) = self + .context + .report_lint(&INVALID_ASSIGNMENT, target.slice.as_ref()) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot assign value of type `{assigned_d}` to key of type `{}` on TypedDict `{value_d}`", + slice_ty.display(db) + )); + attach_original_type_info(&mut diagnostic); + } + } else { + if let Some(builder) = self + .context + .report_lint(&INVALID_KEY, target.slice.as_ref()) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "TypedDict `{value_d}` can only be subscripted with a string literal key, got key of type `{}`.", + slice_ty.display(db) + )); + attach_original_type_info(&mut diagnostic); + } + } + + return false; + }; + + for key in keys { + valid &= TypedDictKeyAssignment { + context: &self.context, + typed_dict, + full_object_ty, + key, + value_ty: rhs_value_ty, + typed_dict_node: target.value.as_ref().into(), + key_node: target.slice.as_ref().into(), + value_node: rhs_value_node.into(), + assignment_kind: TypedDictAssignmentKind::Subscript, + emit_diagnostic, + } + .validate(); + } + + valid + } + + _ => { + let ast_arguments = [ + ast::ArgOrKeyword::Arg(&target.slice), + ast::ArgOrKeyword::Arg(rhs_value_node), + ]; + + let mut call_arguments = + CallArguments::positional([Type::unknown(), Type::unknown()]); + + let mut infer_argument_ty = + |builder: &mut Self, (argument_index, _, tcx): ArgExpr<'db, '_>| { + match argument_index { + 0 => infer_slice_ty(builder, tcx), + 1 => infer_rhs_value(builder, tcx), + _ => unreachable!(), + } + }; + + let Err(call_dunder_err) = self.infer_and_try_call_dunder( + db, + object_ty, + "__setitem__", + ArgumentsIter::synthesized(&ast_arguments), + &mut call_arguments, + &mut infer_argument_ty, + TypeContext::default(), + ) else { + return true; + }; + + let [Some(slice_ty), Some(rhs_value_ty)] = call_arguments.types() else { + unreachable!(); + }; + + match call_dunder_err { + CallDunderError::PossiblyUnbound { .. } => { + if emit_diagnostic + && let Some(builder) = self + .context + .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__setitem__` of type `{}` may be missing", + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } + false + } + CallDunderError::CallError(call_error_kind, bindings) => { + match call_error_kind { + CallErrorKind::NotCallable => { + if emit_diagnostic + && let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__setitem__` of type `{}` is not callable \ + on object of type `{}`", + bindings.callable_type().display(db), + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } + } + CallErrorKind::BindingError => { + if let Some(typed_dict) = object_ty.as_typed_dict() { + if let Some(key) = slice_ty.as_string_literal() { + let key = key.value(db); + TypedDictKeyAssignment { + context: &self.context, + typed_dict, + full_object_ty, + key, + value_ty: *rhs_value_ty, + typed_dict_node: target.value.as_ref().into(), + key_node: target.slice.as_ref().into(), + value_node: rhs_value_node.into(), + assignment_kind: TypedDictAssignmentKind::Subscript, + emit_diagnostic: true, + } + .validate(); + } + } else { + if emit_diagnostic + && let Some(builder) = self.context.report_lint( + &INVALID_ASSIGNMENT, + target.range.cover(rhs_value_node.range()), + ) + { + let assigned_d = rhs_value_ty.display(db); + let object_d = object_ty.display(db); + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid subscript assignment with key of type `{}` and value of \ + type `{assigned_d}` on object of type `{object_d}`", + slice_ty.display(db), + )); + + // Special diagnostic for dictionaries + if let Some([expected_key_ty, expected_value_ty]) = + object_ty + .known_specialization(db, KnownClass::Dict) + .map(|s| s.types(db)) + { + if !slice_ty.is_assignable_to(db, *expected_key_ty) { + diagnostic.annotate( + self.context + .secondary(target.slice.as_ref()) + .message(format_args!( + "Expected key of type `{}`, got `{}`", + expected_key_ty.display(db), + slice_ty.display(db), + )), + ); + } + + if !rhs_value_ty + .is_assignable_to(db, *expected_value_ty) + { + diagnostic.annotate( + self.context.secondary(rhs_value_node).message( + format_args!( + "Expected value of type `{}`, got `{}`", + expected_value_ty.display(db), + rhs_value_ty.display(db), + ), + ), + ); + } + } + + attach_original_type_info(&mut diagnostic); + } + } + } + CallErrorKind::PossiblyNotCallable => { + if emit_diagnostic + && let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__setitem__` of type `{}` may not be callable on object of type `{}`", + bindings.callable_type().display(db), + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } + } + } + false + } + CallDunderError::MethodNotAvailable => { + if emit_diagnostic + && let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot assign to a subscript on an object of type `{}`", + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + + // If it's a user-defined class, suggest adding a `__setitem__` method. + if object_ty + .as_nominal_instance() + .and_then(|instance| instance.class(db).static_class_literal(db)) + .and_then(|(class_literal, _)| { + file_to_module(db, class_literal.file(db)) + }) + .and_then(|module| module.search_path(db)) + .is_some_and(ty_module_resolver::SearchPath::is_first_party) + { + diagnostic.help(format_args!( + "Consider adding a `__setitem__` method to `{}`.", + object_ty.display(db), + )); + } else { + diagnostic.info(format_args!( + "`{}` does not have a `__setitem__` method.", + object_ty.display(db), + )); + } + } + false + } + } + } + } + } + + /// Validate a subscript deletion of the form `del object[key]`. + fn validate_subscript_deletion( + &self, + target: &ast::ExprSubscript, + object_ty: Type<'db>, + slice_ty: Type<'db>, + ) { + self.validate_subscript_deletion_impl(target, None, object_ty, slice_ty); + } + + fn validate_subscript_deletion_impl( + &self, + target: &'ast ast::ExprSubscript, + full_object_ty: Option>, + object_ty: Type<'db>, + slice_ty: Type<'db>, + ) { + let db = self.db(); + + let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { + if let Some(full_object_ty) = full_object_ty { + diagnostic.info(format_args!( + "The full type of the subscripted object is `{}`", + full_object_ty.display(db) + )); + } + }; + + match object_ty { + Type::Union(union) => { + for element_ty in union.elements(db) { + self.validate_subscript_deletion_impl( + target, + full_object_ty.or(Some(object_ty)), + *element_ty, + slice_ty, + ); + } + } + + Type::Intersection(intersection) => { + // Check if any positive element supports deletion + let mut any_valid = false; + for element_ty in intersection.positive(db) { + if self.can_delete_subscript(*element_ty, slice_ty) { + any_valid = true; + break; + } + } + + // If none are valid, emit a diagnostic for the first failing element + if !any_valid && let Some(element_ty) = intersection.positive(db).first() { + self.validate_subscript_deletion_impl( + target, + full_object_ty.or(Some(object_ty)), + *element_ty, + slice_ty, + ); + } + } + + _ => { + match object_ty.try_call_dunder( + db, + "__delitem__", + CallArguments::positional([slice_ty]), + TypeContext::default(), + ) { + Ok(_) => {} + Err(err) => match err { + CallDunderError::PossiblyUnbound { .. } => { + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` may be missing", + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } + } + CallDunderError::CallError(call_error_kind, bindings) => { + match call_error_kind { + CallErrorKind::NotCallable => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` is not callable \ + on object of type `{}`", + bindings.callable_type().display(db), + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } + } + CallErrorKind::BindingError => { + // For deletions of string literal keys on `TypedDict`, provide + // a more detailed diagnostic. + if let Some(typed_dict) = object_ty.as_typed_dict() { + if let Some(string_literal) = slice_ty.as_string_literal() { + let key = string_literal.value(db); + let items = typed_dict.items(db); + + if let Some(field) = items.get(key) { + // Key exists but is required (i.e., can't be deleted). + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + object_ty, + key, + Some(field), + TypedDictDeleteErrorKind::RequiredKey, + ); + } else { + // Key doesn't exist. + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + object_ty, + key, + None, + TypedDictDeleteErrorKind::UnknownKey, + ); + } + } else { + // Non-string-literal key on `TypedDict`. + if let Some(builder) = self + .context + .report_lint(&INVALID_ARGUMENT_TYPE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` cannot be called \ + with key of type `{}` on object of type `{}`", + bindings.callable_type().display(db), + slice_ty.display(db), + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } + } + } else { + // Non-`TypedDict` object + if let Some(builder) = + self.context.report_lint(&INVALID_ARGUMENT_TYPE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` cannot be called \ + with key of type `{}` on object of type `{}`", + bindings.callable_type().display(db), + slice_ty.display(db), + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } + } + } + CallErrorKind::PossiblyNotCallable => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` may not be callable \ + on object of type `{}`", + bindings.callable_type().display(db), + object_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } + } + } + } + CallDunderError::MethodNotAvailable => { + report_not_subscriptable( + &self.context, + target, + object_ty, + "__delitem__", + ); + } + }, + } + } + } + } + + /// Check if a type supports subscript deletion (has `__delitem__`). + fn can_delete_subscript(&self, object_ty: Type<'db>, slice_ty: Type<'db>) -> bool { + let db = self.db(); + object_ty + .try_call_dunder( + db, + "__delitem__", + CallArguments::positional([slice_ty]), + TypeContext::default(), + ) + .is_ok() + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs new file mode 100644 index 0000000000000..06d908365e6b2 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs @@ -0,0 +1,1064 @@ +use crate::{ + Program, + semantic_index::{definition::Definition, scope::NodeWithScopeKind}, + types::{ + BindingContext, KnownClass, KnownInstanceType, LintDiagnosticGuard, Truthiness, Type, + TypeContext, TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, + context::InferContext, + diagnostic::{ + INVALID_LEGACY_TYPE_VARIABLE, INVALID_PARAMSPEC, INVALID_TYPE_VARIABLE_BOUND, + INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, + }, + infer::{ + InferenceFlags, TypeInferenceBuilder, + builder::{BoundOrConstraintsNodes, DeclaredAndInferredType, DeferredExpressionState}, + }, + todo_type, + typevar::{ + TypeVarBoundOrConstraintsEvaluation, TypeVarConstraints, TypeVarDefaultEvaluation, + TypeVarIdentity, TypeVarInstance, + }, + visitor::find_over_type, + }, +}; +use ruff_db::{ + diagnostic::{Annotation, Span}, + parsed::parsed_module, +}; +use ruff_python_ast::{self as ast, PythonVersion}; +use ruff_text_size::{Ranged, TextRange}; + +impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { + pub(super) fn infer_typevar_definition( + &mut self, + node: &ast::TypeParamTypeVar, + definition: Definition<'db>, + ) { + let ast::TypeParamTypeVar { + range: _, + node_index: _, + name, + bound, + default, + } = node; + + let db = self.db(); + + let bound_or_constraint = match bound.as_deref() { + Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { + if elts.len() < 2 { + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, expr) + { + builder.into_diagnostic("TypeVar must have at least two constrained types"); + } + None + } else { + Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints) + } + } + Some(_) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), + None => None, + }; + if bound_or_constraint.is_some() || default.is_some() { + self.deferred.insert(definition, self.multi_inference_state); + } + let identity = TypeVarIdentity::new(db, &name.id, Some(definition), TypeVarKind::Pep695); + let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + db, + identity, + bound_or_constraint, + None, // explicit_variance + default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy), + ))); + self.add_declaration_with_binding( + node.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(ty), + ); + } + + pub(super) fn infer_typevar_deferred(&mut self, node: &'ast ast::TypeParamTypeVar) { + let ast::TypeParamTypeVar { + range: _, + node_index: _, + name, + bound, + default, + } = node; + + let db = self.db(); + + let previous_deferred_state = + std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred); + let bound_node = bound.as_deref(); + let bound_or_constraints = match bound_node { + Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { + // Here, we interpret `bound` as a heterogeneous tuple and convert it to `TypeVarConstraints` + // in `TypeVarInstance::lazy_constraints`. + let constraint_tys: Box<[Type<'_>]> = elts + .iter() + .map(|expr| { + let constraint = self.infer_type_expression(expr); + if constraint.has_typevar_or_typevar_instance(db) + && let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, expr) + { + builder.into_diagnostic("TypeVar constraint cannot be generic"); + } + constraint + }) + .collect(); + + let tuple_ty = Type::heterogeneous_tuple(db, constraint_tys.clone()); + self.store_expression_type(expr, tuple_ty); + // Mirror the `< 2` guard from `infer_typevar_definition` to avoid + // a cascading `invalid-type-variable-default` diagnostic for tuples + // that have already been flagged as invalid constraints. + if elts.len() < 2 { + None + } else { + Some(TypeVarBoundOrConstraints::Constraints( + TypeVarConstraints::new(db, constraint_tys), + )) + } + } + Some(expr) => { + let bound_ty = self.infer_type_expression(expr); + if bound_ty.has_typevar_or_typevar_instance(db) + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_VARIABLE_BOUND, expr) + { + builder.into_diagnostic("TypeVar upper bound cannot be generic"); + } + + Some(TypeVarBoundOrConstraints::UpperBound(bound_ty)) + } + None => None, + }; + if let Some(default_expr) = default.as_deref() { + let default_ty = self.infer_type_expression(default_expr); + if !self.check_default_for_outer_scope_typevars(default_ty, default_expr, &name.id) { + let bound_node = bound_node.map(|n| match n { + ast::Expr::Tuple(tuple) => BoundOrConstraintsNodes::Constraints(&tuple.elts), + _ => BoundOrConstraintsNodes::Bound(n), + }); + self.validate_typevar_default( + Some(&name.id), + bound_or_constraints, + default_ty, + default_expr, + bound_node, + ); + } + } + self.deferred_state = previous_deferred_state; + } + + /// Validate that a `TypeVar`'s default is compatible with its bound or constraints. + pub(super) fn validate_typevar_default( + &mut self, + name: Option<&str>, + bound_or_constraints: Option>, + default_ty: Type<'db>, + default_node: &ast::Expr, + bound_or_constraints_nodes: Option>, + ) { + let Some(bound_or_constraints) = bound_or_constraints else { + return; + }; + + let db = self.db(); + + // Normalize both typevar representations into a `TypeVarInstance` so they + // follow the same compatibility rules: + // - `Type::KnownInstance(TypeVar(..))` for legacy `typing.TypeVar(...)` values + // - `Type::TypeVar(..)` for bound in-scope type parameters (for example, PEP 695) + let default_typevar = match default_ty { + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Some(typevar), + Type::TypeVar(bound_typevar) => Some(bound_typevar.typevar(db)), + _ => None, + }; + + let not_assignable_message = + "TypeVar default is not assignable to the TypeVar's upper bound"; + + let not_assignable_to_upper_bound = || { + self.context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_node) + .map(|builder| { + let mut diagnostic = builder.into_diagnostic(not_assignable_message); + if let Some(BoundOrConstraintsNodes::Bound(bound)) = bound_or_constraints_nodes + { + let secondary = self.context.secondary(bound); + let secondary = if let Some(name) = name { + secondary.message(format_args!("Upper bound of `{name}`")) + } else { + secondary.message("Upper bound of outer TypeVar") + }; + diagnostic.annotate(secondary); + } + diagnostic + }) + }; + + let inconsistent_with_constraints = || { + self.context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_node) + .map(|builder| { + let mut diagnostic = builder.into_diagnostic( + "TypeVar default is inconsistent \ + with the TypeVar's constraints", + ); + if let Some(BoundOrConstraintsNodes::Constraints([first, .., last])) = + bound_or_constraints_nodes + { + let secondary = self + .context + .secondary(TextRange::new(first.start(), last.end())); + let secondary = if let Some(name) = name { + secondary.message(format_args!("Constraints of `{name}`")) + } else { + secondary.message("Constraints of outer TypeVar") + }; + diagnostic.annotate(secondary); + } + diagnostic + }) + }; + + if let Some(default_typevar) = default_typevar { + let default_name = default_typevar.name(db); + + // Annotate the diagnostic with the definition span of the default TypeVar. + let annotate_default_definition = |diagnostic: &mut LintDiagnosticGuard<'_, '_>| { + if let Some(definition) = default_typevar.definition(db) { + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{default_name}` defined here")), + ); + } + }; + + match bound_or_constraints { + TypeVarBoundOrConstraints::UpperBound(outer_bound) => { + // Default TypeVar's upper bound must be assignable to outer's bound. + // If the default has constraints, all constraints must be assignable + // to the outer bound. + if let Some(default_constraints) = default_typevar.constraints(db) { + for constraint in default_constraints { + if !constraint.is_assignable_to(db, outer_bound) { + if let Some(mut diagnostic) = not_assignable_to_upper_bound() { + annotate_default_definition(&mut diagnostic); + if let Some(name) = name { + diagnostic.set_primary_message(format_args!( + "Constraint `{constraint}` of default \ + `{default_name}` is not assignable to upper \ + bound of `{name}`", + constraint = constraint.display(db), + )); + diagnostic.set_concise_message(format_args!( + "Default `{default_name}` of TypeVar `{name}` \ + is not assignable to upper bound `{bound}` \ + of `{name}` because constraint `{constraint}` \ + of `{default_name}` is not assignable to \ + `{bound}`", + bound = outer_bound.display(db), + constraint = constraint.display(db), + )); + } else { + diagnostic.set_primary_message(format_args!( + "Constraint `{constraint}` of `{default_name}` is \ + not assignable to upper bound `{bound}` of \ + outer TypeVar", + constraint = constraint.display(db), + bound = outer_bound.display(db), + )); + diagnostic.set_concise_message(format_args!( + "Default of TypeVar is not assignable its upper \ + bound `{bound}` because constraint `{constraint}` \ + of `{default_name}` is not assignable to `{bound}`", + bound = outer_bound.display(db), + constraint = constraint.display(db), + )); + } + } + break; + } + } + } else { + let default_bound = + default_typevar.upper_bound(db).unwrap_or_else(Type::object); + if !default_bound.is_assignable_to(db, outer_bound) { + if let Some(mut diagnostic) = not_assignable_to_upper_bound() { + annotate_default_definition(&mut diagnostic); + if let Some(name) = name { + diagnostic.set_primary_message(format_args!( + "Upper bound `{default_bound}` of default \ + `{default_name}` is not assignable to upper \ + bound of `{name}`", + default_bound = default_bound.display(db), + )); + diagnostic.set_concise_message(format_args!( + "Default `{default_name}` of TypeVar `{name}` \ + is not assignable to upper bound `{bound}` \ + of `{name}` because its upper bound \ + `{default_bound}` is not assignable to \ + `{bound}`", + bound = outer_bound.display(db), + default_bound = default_bound.display(db), + )); + } else { + diagnostic.set_primary_message(format_args!( + "Upper bound `{default_bound}` of default \ + `{default_name}` is not assignable to upper \ + bound of outer TypeVar", + default_bound = default_bound.display(db), + )); + diagnostic.set_concise_message(format_args!( + "TypeVar default `{default_name}` is not \ + assignable to upper bound `{bound}` \ + because upper bound of `{default_name}` + (`{default_bound}`) is not assignable + to `{bound}`", + bound = outer_bound.display(db), + default_bound = default_bound.display(db), + )); + } + } + } + } + } + TypeVarBoundOrConstraints::Constraints(outer_constraints) => { + // TypeVar default with constrained outer. + let outer = outer_constraints.elements(db); + if let Some(default_constraints) = default_typevar.constraints(db) { + // Default has constraints: outer constraints must be a superset. + for default_constraint in default_constraints { + if !outer + .iter() + .any(|o| default_constraint.is_equivalent_to(db, *o)) + { + if let Some(mut diagnostic) = inconsistent_with_constraints() { + annotate_default_definition(&mut diagnostic); + if let Some(name) = name { + diagnostic.set_primary_message(format_args!( + "Constraint `{constraint}` of default \ + `{default_name}` is not one of the constraints \ + of `{name}`", + constraint = default_constraint.display(db), + )); + diagnostic.set_concise_message(format_args!( + "Default `{default_name}` of TypeVar `{name}` \ + is inconsistent with its constraints \ + `{name}` because constraint `{constraint}` of \ + `{default_name}` is not one of the constraints \ + of `{name}`", + constraint = default_constraint.display(db), + )); + } else { + diagnostic.set_primary_message(format_args!( + "Constraint `{constraint}` of outer TypeVar default \ + `{default_name}` is not one of the constraints \ + of the outer TypeVar", + constraint = default_constraint.display(db), + )); + diagnostic.set_concise_message(format_args!( + "Default `{default_name}` of outer TypeVar is \ + inconsistent with the constraints of the outer \ + TypeVar because constraint `{constraint}` of \ + default `{default_name}` is not one of the \ + constraints of the outer TypeVar", + constraint = default_constraint.display(db), + )); + } + } + break; + } + } + } else { + // A non-constrained default TypeVar (bounded or unbounded) is + // incompatible with a constrained outer TypeVar per the typing spec. + if let Some(mut diagnostic) = inconsistent_with_constraints() { + annotate_default_definition(&mut diagnostic); + if let Some(default_bound) = default_typevar.upper_bound(db) { + diagnostic.set_primary_message( + "Bounded TypeVar cannot be used as the default \ + for a constrained TypeVar", + ); + diagnostic.info(format_args!( + "`{default_name}` has bound `{default_bound}` but is not constrained", + default_bound = default_bound.display(db), + )); + } else { + diagnostic.set_primary_message( + "Unbounded TypeVar cannot be used as the default \ + for a constrained TypeVar", + ); + diagnostic.info(format_args!( + "`{default_name}` has no bound or constraints", + )); + } + } + } + } + } + return; + } + + // Concrete default type checks. + match bound_or_constraints { + TypeVarBoundOrConstraints::UpperBound(bound) => { + if !default_ty.is_assignable_to(db, bound) { + if let Some(mut diagnostic) = not_assignable_to_upper_bound() { + if let Some(name) = name { + diagnostic.set_primary_message(format_args!("Default of `{name}`")); + } else { + diagnostic.set_primary_message("TypeVar default"); + } + diagnostic.set_concise_message(not_assignable_message); + } + } + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + if default_ty != Type::any() + && !constraints + .elements(db) + .iter() + .any(|c| default_ty.is_equivalent_to(db, *c)) + { + if let Some(mut diagnostic) = inconsistent_with_constraints() { + if let Some(name) = name { + diagnostic.set_primary_message(format_args!( + "`{default}` is not one of the constraints of `{name}`", + default = default_ty.display(db), + )); + } else { + diagnostic.set_primary_message(format_args!( + "`{default}` is not one of the constraints", + default = default_ty.display(db), + )); + } + } + } + } + } + } + + /// Check if a PEP 695 type parameter's default references type variables from an outer scope. + /// + /// Returns `true` if such a reference was found and a diagnostic was emitted, + /// indicating that further default validation should be skipped. + /// + /// Note: this only handles PEP 695 type parameters in function and type alias scopes. + /// Class type parameter scopes are skipped here because out-of-scope references + /// are validated at the class level via `report_invalid_typevar_default_reference`. + /// Legacy `TypeVar`s are validated by `check_legacy_typevar_defaults`. + fn check_default_for_outer_scope_typevars( + &self, + default_ty: Type<'db>, + default_node: &ast::Expr, + typevar_name: &str, + ) -> bool { + let db = self.db(); + + // Determine the expected binding context from the current type parameter scope. + // Only check function and type alias scopes; class scopes are handled separately + // when processing the class definition. + let expected_binding_def = match self.scope().node(db) { + NodeWithScopeKind::FunctionTypeParameters(function) => { + self.index.expect_single_definition(function) + } + NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => { + self.index.expect_single_definition(type_alias) + } + _ => return false, + }; + let expected_binding = BindingContext::Definition(expected_binding_def); + + let outer_tv = find_over_type(db, default_ty, false, |ty| { + if let Type::TypeVar(bound_tv) = ty + && bound_tv.binding_context(db) != expected_binding + { + Some(bound_tv) + } else { + None + } + }); + + let Some(outer_tv) = outer_tv else { + return false; + }; + let outer_typevar = outer_tv.typevar(db); + let outer_name = outer_typevar.name(db); + let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_node) + else { + return false; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid default for type parameter `{typevar_name}`" + )); + diagnostic.set_primary_message(format_args!( + "`{outer_name}` is a type parameter bound in an outer scope" + )); + diagnostic.set_concise_message(format_args!( + "Type parameter `{typevar_name}` cannot use \ + outer-scope type parameter `{outer_name}` as its default" + )); + if let Some(definition) = outer_typevar.definition(db) { + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{outer_name}` defined here")), + ); + } + diagnostic.info("See https://typing.python.org/en/latest/spec/generics.html#scoping-rules"); + + true + } + + pub(super) fn infer_paramspec_definition( + &mut self, + node: &ast::TypeParamParamSpec, + definition: Definition<'db>, + ) { + let ast::TypeParamParamSpec { + range: _, + node_index: _, + name, + default, + } = node; + + let db = self.db(); + + if default.is_some() { + self.deferred.insert(definition, self.multi_inference_state); + } + let identity = + TypeVarIdentity::new(db, &name.id, Some(definition), TypeVarKind::Pep695ParamSpec); + let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + db, + identity, + None, // ParamSpec, when declared using PEP 695 syntax, has no bounds or constraints + None, // explicit_variance + default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy), + ))); + self.add_declaration_with_binding( + node.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(ty), + ); + } + + pub(super) fn infer_paramspec_deferred(&mut self, node: &ast::TypeParamParamSpec) { + let ast::TypeParamParamSpec { + range: _, + node_index: _, + name, + default: Some(default), + } = node + else { + return; + }; + let previous_deferred_state = + std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred); + self.infer_paramspec_default(default, Some(&name.id)); + self.deferred_state = previous_deferred_state; + } + + pub(super) fn infer_paramspec_default( + &mut self, + default_expr: &ast::Expr, + paramspec_name: Option<&str>, + ) { + let previously_allowed_paramspec = self + .inference_flags + .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true); + self.infer_paramspec_default_impl(default_expr, paramspec_name); + self.inference_flags.set( + InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, + previously_allowed_paramspec, + ); + } + + fn infer_paramspec_default_impl( + &mut self, + default_expr: &ast::Expr, + paramspec_name: Option<&str>, + ) { + let db = self.db(); + + match default_expr { + ast::Expr::EllipsisLiteral(ellipsis) => { + let ty = self.infer_ellipsis_literal_expression(ellipsis); + self.store_expression_type(default_expr, ty); + return; + } + ast::Expr::List(ast::ExprList { elts, .. }) => { + let types = elts + .iter() + .map(|elt| self.infer_type_expression(elt)) + .collect::>(); + // N.B. We cannot represent a heterogeneous list of types in our type system, so we + // use a heterogeneous tuple type to represent the list of types instead. + self.store_expression_type(default_expr, Type::heterogeneous_tuple(db, types)); + return; + } + ast::Expr::Name(_) => { + let ty = self.infer_type_expression(default_expr); + if let Some(name) = paramspec_name + && self.check_default_for_outer_scope_typevars(ty, default_expr, name) + { + return; + } + let is_paramspec = match ty { + Type::TypeVar(typevar) => typevar.is_paramspec(db), + Type::KnownInstance(known_instance) => { + known_instance.class(db) == KnownClass::ParamSpec + } + _ => false, + }; + if is_paramspec { + return; + } + } + _ => {} + } + if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, default_expr) { + builder.into_diagnostic( + "The default value to `ParamSpec` must be either \ + a list of types, `ParamSpec`, or `...`", + ); + } + } + + pub(super) fn infer_typevartuple_definition( + &mut self, + node: &ast::TypeParamTypeVarTuple, + definition: Definition<'db>, + ) { + let ast::TypeParamTypeVarTuple { + range: _, + node_index: _, + name: _, + default, + } = node; + self.infer_optional_expression(default.as_deref(), TypeContext::default()); + let pep_695_todo = todo_type!("PEP-695 TypeVarTuple definition types"); + self.add_declaration_with_binding( + node.into(), + definition, + &DeclaredAndInferredType::are_the_same_type(pep_695_todo), + ); + } + + pub(super) fn infer_legacy_paramspec( + &mut self, + target: &ast::Expr, + call_expr: &ast::ExprCall, + definition: Definition<'db>, + known_class: KnownClass, + ) -> Type<'db> { + fn error<'db>( + context: &InferContext<'db, '_>, + message: impl std::fmt::Display, + node: impl Ranged, + ) -> Type<'db> { + if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, node) { + builder.into_diagnostic(message); + } + // If the call doesn't create a valid paramspec, we'll emit diagnostics and fall back to + // just creating a regular instance of `typing.ParamSpec`. + KnownClass::ParamSpec.to_instance(context.db()) + } + + let db = self.db(); + let arguments = &call_expr.arguments; + let is_typing_extensions = known_class == KnownClass::ExtensionsParamSpec; + let assume_all_features = self.in_stub() || is_typing_extensions; + let python_version = Program::get(db).python_version(db); + let have_features_from = + |version: PythonVersion| assume_all_features || python_version >= version; + + let mut default = None; + let mut name_param_ty = None; + + if arguments.args.len() > 1 { + return error( + &self.context, + "`ParamSpec` can only have one positional argument", + call_expr, + ); + } + + if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { + return error( + &self.context, + "Starred arguments are not supported in `ParamSpec` creation", + starred, + ); + } + + for kwarg in &arguments.keywords { + let Some(identifier) = kwarg.arg.as_ref() else { + return error( + &self.context, + "Starred arguments are not supported in `ParamSpec` creation", + kwarg, + ); + }; + match identifier.id().as_str() { + "name" => { + // Duplicate keyword argument is a syntax error, so we don't have to check if + // `name_param_ty.is_some()` here. + if !arguments.args.is_empty() { + return error( + &self.context, + "The `name` parameter of `ParamSpec` can only be provided once", + kwarg, + ); + } + name_param_ty = + Some(self.infer_expression(&kwarg.value, TypeContext::default())); + } + "bound" | "covariant" | "contravariant" | "infer_variance" => { + return error( + &self.context, + "The variance and bound arguments for `ParamSpec` do not have defined semantics yet", + call_expr, + ); + } + "default" => { + if !have_features_from(PythonVersion::PY313) { + // We don't return here; this error is informational since this will error + // at runtime, but the user's intent is plain, we may as well respect it. + error( + &self.context, + "The `default` parameter of `typing.ParamSpec` was added in Python 3.13", + kwarg, + ); + } + default = Some(TypeVarDefaultEvaluation::Lazy); + } + name => { + // We don't return here; this error is informational since this will error + // at runtime, but it will likely cause fewer cascading errors if we just + // ignore the unknown keyword and still understand as much of the typevar as we + // can. + error( + &self.context, + format_args!("Unknown keyword argument `{name}` in `ParamSpec` creation"), + kwarg, + ); + self.infer_expression(&kwarg.value, TypeContext::default()); + } + } + } + + let Some(name_param_ty) = name_param_ty.or_else(|| { + arguments + .find_positional(0) + .map(|arg| self.infer_expression(arg, TypeContext::default())) + }) else { + return error( + &self.context, + "The `name` parameter of `ParamSpec` is required.", + call_expr, + ); + }; + + let Some(name_param) = name_param_ty.as_string_literal().map(|name| name.value(db)) else { + return error( + &self.context, + "The first argument to `ParamSpec` must be a string literal", + call_expr, + ); + }; + + let ast::Expr::Name(ast::ExprName { + id: target_name, .. + }) = target + else { + return error( + &self.context, + "A `ParamSpec` definition must be a simple variable assignment", + target, + ); + }; + + if name_param != target_name { + return error( + &self.context, + format_args!( + "The name of a `ParamSpec` (`{name_param}`) must match \ + the name of the variable it is assigned to (`{target_name}`)" + ), + target, + ); + } + + if default.is_some() { + self.deferred.insert(definition, self.multi_inference_state); + } + + let identity = + TypeVarIdentity::new(db, target_name, Some(definition), TypeVarKind::ParamSpec); + Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + db, identity, None, None, default, + ))) + } + + pub(super) fn infer_legacy_typevar( + &mut self, + target: &ast::Expr, + call_expr: &ast::ExprCall, + definition: Definition<'db>, + known_class: KnownClass, + ) -> Type<'db> { + fn error<'db>( + context: &InferContext<'db, '_>, + message: impl std::fmt::Display, + node: impl Ranged, + ) -> Type<'db> { + if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) { + builder.into_diagnostic(message); + } + // If the call doesn't create a valid typevar, we'll emit diagnostics and fall back to + // just creating a regular instance of `typing.TypeVar`. + KnownClass::TypeVar.to_instance(context.db()) + } + + let db = self.db(); + let arguments = &call_expr.arguments; + let is_typing_extensions = known_class == KnownClass::ExtensionsTypeVar; + let assume_all_features = self.in_stub() || is_typing_extensions; + let python_version = Program::get(db).python_version(db); + let have_features_from = + |version: PythonVersion| assume_all_features || python_version >= version; + + let mut has_bound = false; + let mut default = None; + let mut covariant = false; + let mut contravariant = false; + let mut name_param_ty = None; + + if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { + return error( + &self.context, + "Starred arguments are not supported in `TypeVar` creation", + starred, + ); + } + + for kwarg in &arguments.keywords { + let Some(identifier) = kwarg.arg.as_ref() else { + return error( + &self.context, + "Starred arguments are not supported in `TypeVar` creation", + kwarg, + ); + }; + match identifier.id().as_str() { + "name" => { + // Duplicate keyword argument is a syntax error, so we don't have to check if + // `name_param_ty.is_some()` here. + if !arguments.args.is_empty() { + return error( + &self.context, + "The `name` parameter of `TypeVar` can only be provided once.", + kwarg, + ); + } + name_param_ty = + Some(self.infer_expression(&kwarg.value, TypeContext::default())); + } + "bound" => has_bound = true, + "covariant" => { + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => covariant = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `covariant` parameter of `TypeVar` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + } + "contravariant" => { + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => contravariant = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `contravariant` parameter of `TypeVar` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + } + "default" => { + if !have_features_from(PythonVersion::PY313) { + // We don't return here; this error is informational since this will error + // at runtime, but the user's intent is plain, we may as well respect it. + error( + &self.context, + "The `default` parameter of `typing.TypeVar` was added in Python 3.13", + kwarg, + ); + } + + default = Some(TypeVarDefaultEvaluation::Lazy); + } + "infer_variance" => { + if !have_features_from(PythonVersion::PY312) { + // We don't return here; this error is informational since this will error + // at runtime, but the user's intent is plain, we may as well respect it. + error( + &self.context, + "The `infer_variance` parameter of `typing.TypeVar` was added in Python 3.12", + kwarg, + ); + } + // TODO support `infer_variance` in legacy TypeVars + if self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + .is_ambiguous() + { + return error( + &self.context, + "The `infer_variance` parameter of `TypeVar` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + name => { + // We don't return here; this error is informational since this will error + // at runtime, but it will likely cause fewer cascading errors if we just + // ignore the unknown keyword and still understand as much of the typevar as we + // can. + error( + &self.context, + format_args!("Unknown keyword argument `{name}` in `TypeVar` creation",), + kwarg, + ); + self.infer_expression(&kwarg.value, TypeContext::default()); + } + } + } + + let variance = match (covariant, contravariant) { + (true, true) => { + return error( + &self.context, + "A `TypeVar` cannot be both covariant and contravariant", + call_expr, + ); + } + (true, false) => TypeVarVariance::Covariant, + (false, true) => TypeVarVariance::Contravariant, + (false, false) => TypeVarVariance::Invariant, + }; + + let Some(name_param_ty) = name_param_ty.or_else(|| { + arguments + .find_positional(0) + .map(|arg| self.infer_expression(arg, TypeContext::default())) + }) else { + return error( + &self.context, + "The `name` parameter of `TypeVar` is required.", + call_expr, + ); + }; + + let Some(name_param) = name_param_ty.as_string_literal().map(|name| name.value(db)) else { + return error( + &self.context, + "The first argument to `TypeVar` must be a string literal.", + call_expr, + ); + }; + + let ast::Expr::Name(ast::ExprName { + id: target_name, .. + }) = target + else { + return error( + &self.context, + "A `TypeVar` definition must be a simple variable assignment", + target, + ); + }; + + if name_param != target_name { + return error( + &self.context, + format_args!( + "The name of a `TypeVar` (`{name_param}`) must match \ + the name of the variable it is assigned to (`{target_name}`)" + ), + target, + ); + } + + // Inference of bounds, constraints, and defaults must be deferred, to avoid cycles. So we + // only check presence/absence/number here. + + let num_constraints = arguments.args.len().saturating_sub(1); + + let bound_or_constraints = match (has_bound, num_constraints) { + (false, 0) => None, + (true, 0) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), + (true, _) => { + return error( + &self.context, + "A `TypeVar` cannot have both a bound and constraints", + call_expr, + ); + } + (_, 1) => { + return error( + &self.context, + "A `TypeVar` cannot have exactly one constraint", + &arguments.args[1], + ); + } + (false, _) => Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints), + }; + + if bound_or_constraints.is_some() || default.is_some() { + self.deferred.insert(definition, self.multi_inference_state); + } + + let identity = TypeVarIdentity::new(db, target_name, Some(definition), TypeVarKind::Legacy); + Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + db, + identity, + bound_or_constraints, + Some(variance), + default, + ))) + } +} From 4e7c8dbfc146852778a193d218dd03646f1a3286 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 8 Mar 2026 12:26:26 -0400 Subject: [PATCH 243/261] Retain `lazy` keyword when sorting imports (#23762) ## Summary For now, we keep our sections as-is, but within each section, we show all the eager imports followed by all the lazy imports, e.g.: ```python import json import os import subprocess from collections import defaultdict from pathlib import Path from typing import Final lazy import ast lazy import shutil lazy from dataclasses import dataclass ``` We may change this behavior later; it shouldn't be considered stable. See: https://github.com/astral-sh/ruff/issues/21305. --- .../isort/lazy_force_sort_within_sections.py | 4 + .../test/fixtures/isort/lazy_from_first.py | 4 + .../test/fixtures/isort/lazy_imports.py | 4 + .../ruff_linter/src/rules/isort/annotate.rs | 6 +- crates/ruff_linter/src/rules/isort/format.rs | 14 +- crates/ruff_linter/src/rules/isort/mod.rs | 4 + .../ruff_linter/src/rules/isort/normalize.rs | 59 +++- crates/ruff_linter/src/rules/isort/order.rs | 254 +++++++++++++----- ...ns_lazy_force_sort_within_sections.py.snap | 19 ++ ..._tests__from_first_lazy_from_first.py.snap | 21 ++ ..._rules__isort__tests__lazy_imports.py.snap | 19 ++ crates/ruff_linter/src/rules/isort/types.rs | 4 + 12 files changed, 332 insertions(+), 80 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/lazy_force_sort_within_sections.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/lazy_from_first.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/lazy_imports.py create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_lazy_force_sort_within_sections.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__from_first_lazy_from_first.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lazy_imports.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/isort/lazy_force_sort_within_sections.py b/crates/ruff_linter/resources/test/fixtures/isort/lazy_force_sort_within_sections.py new file mode 100644 index 0000000000000..63903fe469f23 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/lazy_force_sort_within_sections.py @@ -0,0 +1,4 @@ +lazy from math import pi +from math import pi +lazy import os +import os diff --git a/crates/ruff_linter/resources/test/fixtures/isort/lazy_from_first.py b/crates/ruff_linter/resources/test/fixtures/isort/lazy_from_first.py new file mode 100644 index 0000000000000..63903fe469f23 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/lazy_from_first.py @@ -0,0 +1,4 @@ +lazy from math import pi +from math import pi +lazy import os +import os diff --git a/crates/ruff_linter/resources/test/fixtures/isort/lazy_imports.py b/crates/ruff_linter/resources/test/fixtures/isort/lazy_imports.py new file mode 100644 index 0000000000000..63903fe469f23 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/lazy_imports.py @@ -0,0 +1,4 @@ +lazy from math import pi +from math import pi +lazy import os +import os diff --git a/crates/ruff_linter/src/rules/isort/annotate.rs b/crates/ruff_linter/src/rules/isort/annotate.rs index 122781898a7c9..b70c665925a2b 100644 --- a/crates/ruff_linter/src/rules/isort/annotate.rs +++ b/crates/ruff_linter/src/rules/isort/annotate.rs @@ -26,7 +26,7 @@ pub(crate) fn annotate_imports<'a>( Stmt::Import(ast::StmtImport { names, range, - is_lazy: _, + is_lazy, node_index: _, }) => { // Find comments above. @@ -53,6 +53,7 @@ pub(crate) fn annotate_imports<'a>( .map(|alias| AliasData { name: locator.slice(&alias.name), asname: alias.asname.as_ref().map(|asname| locator.slice(asname)), + is_lazy: *is_lazy, }) .collect(), atop, @@ -63,7 +64,7 @@ pub(crate) fn annotate_imports<'a>( module, names, level, - is_lazy: _, + is_lazy, range: _, node_index: _, }) => { @@ -158,6 +159,7 @@ pub(crate) fn annotate_imports<'a>( module: module.as_ref().map(|module| locator.slice(module)), names: aliases, level: *level, + is_lazy: *is_lazy, trailing_comma: if split_on_trailing_comma { trailing_comma(import, tokens) } else { diff --git a/crates/ruff_linter/src/rules/isort/format.rs b/crates/ruff_linter/src/rules/isort/format.rs index e7210cd8dabee..8cd811091c87e 100644 --- a/crates/ruff_linter/src/rules/isort/format.rs +++ b/crates/ruff_linter/src/rules/isort/format.rs @@ -22,6 +22,9 @@ pub(crate) fn format_import( output.push_str(comment); output.push_str(&stylist.line_ending()); } + if alias.is_lazy { + output.push_str("lazy "); + } if let Some(asname) = alias.asname { output.push_str("import "); output.push_str(alias.name); @@ -124,12 +127,16 @@ fn format_single_line( } let module_name = import_from.module_name(); + if import_from.is_lazy { + output.push_str("lazy "); + line_width = line_width.add_width(5); + } output.push_str("from "); output.push_str(&module_name); output.push_str(" import "); line_width = line_width.add_width(5).add_str(&module_name).add_width(8); - for (index, (AliasData { name, asname }, _)) in aliases.iter().enumerate() { + for (index, (AliasData { name, asname, .. }, _)) in aliases.iter().enumerate() { if let Some(asname) = asname { output.push_str(name); output.push_str(" as "); @@ -205,6 +212,9 @@ fn format_multi_line( output.push_str(&stylist.line_ending()); } + if import_from.is_lazy { + output.push_str("lazy "); + } output.push_str("from "); output.push_str(&import_from.module_name()); output.push_str(" import "); @@ -216,7 +226,7 @@ fn format_multi_line( } output.push_str(&stylist.line_ending()); - for (AliasData { name, asname }, comments) in aliases { + for (AliasData { name, asname, .. }, comments) in aliases { for comment in &comments.atop { output.push_str(stylist.indentation()); output.push_str(comment); diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 76cb8314004f6..aaff4cebcf457 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -56,6 +56,7 @@ pub(crate) enum AnnotatedImport<'a> { module: Option<&'a str>, names: Vec>, level: u32, + is_lazy: bool, atop: Vec>, inline: Vec>, trailing: Vec>, @@ -341,6 +342,7 @@ mod tests { #[test_case(Path::new("insert_empty_lines.py"))] #[test_case(Path::new("insert_empty_lines.pyi"))] #[test_case(Path::new("isort_skip_file.py"))] + #[test_case(Path::new("lazy_imports.py"))] #[test_case(Path::new("leading_prefix.py"))] #[test_case(Path::new("magic_trailing_comma.py"))] #[test_case(Path::new("match_case.py"))] @@ -766,6 +768,7 @@ mod tests { } #[test_case(Path::new("force_sort_within_sections.py"))] + #[test_case(Path::new("lazy_force_sort_within_sections.py"))] #[test_case(Path::new("force_sort_within_sections_with_as_names.py"))] #[test_case(Path::new("force_sort_within_sections_future.py"))] fn force_sort_within_sections(path: &Path) -> Result<()> { @@ -1064,6 +1067,7 @@ mod tests { } #[test_case(Path::new("from_first.py"))] + #[test_case(Path::new("lazy_from_first.py"))] fn from_first(path: &Path) -> Result<()> { let snapshot = format!("from_first_{}", path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/isort/normalize.rs b/crates/ruff_linter/src/rules/isort/normalize.rs index b59113a11d052..3ac813a532db7 100644 --- a/crates/ruff_linter/src/rules/isort/normalize.rs +++ b/crates/ruff_linter/src/rules/isort/normalize.rs @@ -7,7 +7,7 @@ pub(crate) fn normalize_imports<'a>( settings: &Settings, ) -> ImportBlock<'a> { let mut block = ImportBlock::default(); - for import in imports { + for (index, import) in imports.into_iter().enumerate() { match import { AnnotatedImport::Import { names, @@ -21,8 +21,10 @@ pub(crate) fn normalize_imports<'a>( .entry(AliasData { name: name.name, asname: name.asname, + is_lazy: name.is_lazy, }) .or_default(); + comment_set.first_index.get_or_insert(index); for comment in atop { comment_set.atop.push(comment.value); } @@ -38,14 +40,18 @@ pub(crate) fn normalize_imports<'a>( .entry(AliasData { name: name.name, asname: name.asname, + is_lazy: name.is_lazy, }) - .or_default(); + .or_default() + .first_index + .get_or_insert(index); } } AnnotatedImport::ImportFrom { module, names, level, + is_lazy, atop, inline, trailing, @@ -64,13 +70,19 @@ pub(crate) fn normalize_imports<'a>( let import_from = block .import_from_as .entry(( - ImportFromData { module, level }, + ImportFromData { + module, + level, + is_lazy, + }, AliasData { name: alias.name, asname: alias.asname, + is_lazy: false, }, )) .or_default(); + import_from.first_index.get_or_insert(index); // Associate the comments above the import statement with the first alias // (best effort). @@ -93,25 +105,39 @@ pub(crate) fn normalize_imports<'a>( let import_from = if alias.name == "*" { block .import_from_star - .entry(ImportFromData { module, level }) + .entry(ImportFromData { + module, + level, + is_lazy, + }) .or_default() } else if alias.asname.is_none() || settings.combine_as_imports { block .import_from - .entry(ImportFromData { module, level }) + .entry(ImportFromData { + module, + level, + is_lazy, + }) .or_default() } else { block .import_from_as .entry(( - ImportFromData { module, level }, + ImportFromData { + module, + level, + is_lazy, + }, AliasData { name: alias.name, asname: alias.asname, + is_lazy: false, }, )) .or_default() }; + import_from.first_index.get_or_insert(index); for comment in atop { import_from.comments.atop.push(comment.value); @@ -130,33 +156,48 @@ pub(crate) fn normalize_imports<'a>( let import_from = if alias.name == "*" { block .import_from_star - .entry(ImportFromData { module, level }) + .entry(ImportFromData { + module, + level, + is_lazy, + }) .or_default() } else if !isolate_aliases && (alias.asname.is_none() || settings.combine_as_imports) { block .import_from - .entry(ImportFromData { module, level }) + .entry(ImportFromData { + module, + level, + is_lazy, + }) .or_default() } else { block .import_from_as .entry(( - ImportFromData { module, level }, + ImportFromData { + module, + level, + is_lazy, + }, AliasData { name: alias.name, asname: alias.asname, + is_lazy: false, }, )) .or_default() }; + import_from.first_index.get_or_insert(index); let comment_set = import_from .aliases .entry(AliasData { name: alias.name, asname: alias.asname, + is_lazy: false, }) .or_default(); diff --git a/crates/ruff_linter/src/rules/isort/order.rs b/crates/ruff_linter/src/rules/isort/order.rs index fca47f8775d24..40b74662bb33e 100644 --- a/crates/ruff_linter/src/rules/isort/order.rs +++ b/crates/ruff_linter/src/rules/isort/order.rs @@ -34,6 +34,7 @@ pub(crate) fn order_imports<'a>( |( import_from, ImportFromStatement { + first_index, comments, aliases, trailing_comma, @@ -42,6 +43,7 @@ pub(crate) fn order_imports<'a>( // Within each `Stmt::ImportFrom`, sort the members. ( import_from, + first_index.unwrap_or_default(), comments, trailing_comma, aliases @@ -55,89 +57,207 @@ pub(crate) fn order_imports<'a>( ); if matches!(section, ImportSection::Known(ImportType::Future)) { - from_imports - .sorted_by_cached_key(|(import_from, _, _, aliases)| { - ModuleKey::from_module( - import_from.module, - None, - import_from.level, - aliases.first().map(|(alias, _)| (alias.name, alias.asname)), - ImportStyle::From, - settings, + let ordered_from_imports = from_imports + .sorted_by_cached_key(|(import_from, first_index, _, _, aliases)| { + ( + ModuleKey::from_module( + import_from.module, + None, + import_from.level, + aliases.first().map(|(alias, _)| (alias.name, alias.asname)), + ImportStyle::From, + settings, + ), + *first_index, ) }) - .map(ImportFrom) - .chain( - straight_imports - .sorted_by_cached_key(|(alias, _)| { - ModuleKey::from_module( - Some(alias.name), - alias.asname, - 0, - None, - ImportStyle::Straight, - settings, - ) - }) - .map(Import), - ) + .collect::>(); + let mut eager_from_imports = vec![]; + let mut lazy_from_imports = vec![]; + for import_from in ordered_from_imports { + if import_from.0.is_lazy { + lazy_from_imports.push(import_from); + } else { + eager_from_imports.push(import_from); + } + } + + let ordered_straight_imports = straight_imports + .sorted_by_cached_key(|(alias, comments)| { + ( + ModuleKey::from_module( + Some(alias.name), + alias.asname, + 0, + None, + ImportStyle::Straight, + settings, + ), + comments.first_index.unwrap_or_default(), + ) + }) + .collect::>(); + let mut eager_straight_imports = vec![]; + let mut lazy_straight_imports = vec![]; + for import in ordered_straight_imports { + if import.0.is_lazy { + lazy_straight_imports.push(import); + } else { + eager_straight_imports.push(import); + } + } + + eager_from_imports + .into_iter() + .map(|(import_from, _, comments, trailing_comma, aliases)| { + ImportFrom((import_from, comments, trailing_comma, aliases)) + }) + .chain(eager_straight_imports.into_iter().map(Import)) + .chain(lazy_from_imports.into_iter().map( + |(import_from, _, comments, trailing_comma, aliases)| { + ImportFrom((import_from, comments, trailing_comma, aliases)) + }, + )) + .chain(lazy_straight_imports.into_iter().map(Import)) .collect() } else if settings.force_sort_within_sections { - straight_imports - .map(Import) - .chain(from_imports.map(ImportFrom)) - .sorted_by_cached_key(|import| match import { - Import((alias, _)) => ModuleKey::from_module( - Some(alias.name), - alias.asname, - 0, - None, - ImportStyle::Straight, - settings, + let ordered_imports = straight_imports + .map(|(alias, comments)| { + let first_index = comments.first_index.unwrap_or_default(); + (Import((alias, comments)), first_index) + }) + .chain(from_imports.map( + |(import_from, first_index, comments, trailing_comma, aliases)| { + ( + ImportFrom((import_from, comments, trailing_comma, aliases)), + first_index, + ) + }, + )) + .sorted_by_cached_key(|(import, first_index)| match import { + Import((alias, _)) => ( + ModuleKey::from_module( + Some(alias.name), + alias.asname, + 0, + None, + ImportStyle::Straight, + settings, + ), + *first_index, ), - ImportFrom((import_from, _, _, aliases)) => ModuleKey::from_module( - import_from.module, - None, - import_from.level, - aliases.first().map(|(alias, _)| (alias.name, alias.asname)), - ImportStyle::From, - settings, + ImportFrom((import_from, _, _, aliases)) => ( + ModuleKey::from_module( + import_from.module, + None, + import_from.level, + aliases.first().map(|(alias, _)| (alias.name, alias.asname)), + ImportStyle::From, + settings, + ), + *first_index, ), }) + .collect::>(); + + let mut eager_imports = vec![]; + let mut lazy_imports = vec![]; + for (import, first_index) in ordered_imports { + let is_lazy = match &import { + Import((alias, _)) => alias.is_lazy, + ImportFrom((import_from, _, _, _)) => import_from.is_lazy, + }; + if is_lazy { + lazy_imports.push((import, first_index)); + } else { + eager_imports.push((import, first_index)); + } + } + + eager_imports + .into_iter() + .chain(lazy_imports) + .map(|(import, _)| import) .collect() } else { - let ordered_straight_imports = straight_imports.sorted_by_cached_key(|(alias, _)| { - ModuleKey::from_module( - Some(alias.name), - alias.asname, - 0, - None, - ImportStyle::Straight, - settings, - ) - }); - let ordered_from_imports = - from_imports.sorted_by_cached_key(|(import_from, _, _, aliases)| { - ModuleKey::from_module( - import_from.module, - None, - import_from.level, - aliases.first().map(|(alias, _)| (alias.name, alias.asname)), - ImportStyle::From, - settings, + let ordered_straight_imports = straight_imports + .sorted_by_cached_key(|(alias, comments)| { + ( + ModuleKey::from_module( + Some(alias.name), + alias.asname, + 0, + None, + ImportStyle::Straight, + settings, + ), + comments.first_index.unwrap_or_default(), + ) + }) + .collect::>(); + let mut eager_straight_imports = vec![]; + let mut lazy_straight_imports = vec![]; + for import in ordered_straight_imports { + if import.0.is_lazy { + lazy_straight_imports.push(import); + } else { + eager_straight_imports.push(import); + } + } + + let ordered_from_imports = from_imports + .sorted_by_cached_key(|(import_from, first_index, _, _, aliases)| { + ( + ModuleKey::from_module( + import_from.module, + None, + import_from.level, + aliases.first().map(|(alias, _)| (alias.name, alias.asname)), + ImportStyle::From, + settings, + ), + *first_index, ) - }); + }) + .collect::>(); + let mut eager_from_imports = vec![]; + let mut lazy_from_imports = vec![]; + for import_from in ordered_from_imports { + if import_from.0.is_lazy { + lazy_from_imports.push(import_from); + } else { + eager_from_imports.push(import_from); + } + } if settings.from_first { - ordered_from_imports + eager_from_imports .into_iter() - .map(ImportFrom) - .chain(ordered_straight_imports.into_iter().map(Import)) + .map(|(import_from, _, comments, trailing_comma, aliases)| { + ImportFrom((import_from, comments, trailing_comma, aliases)) + }) + .chain(eager_straight_imports.into_iter().map(Import)) + .chain(lazy_from_imports.into_iter().map( + |(import_from, _, comments, trailing_comma, aliases)| { + ImportFrom((import_from, comments, trailing_comma, aliases)) + }, + )) + .chain(lazy_straight_imports.into_iter().map(Import)) .collect() } else { - ordered_straight_imports + eager_straight_imports .into_iter() .map(Import) - .chain(ordered_from_imports.into_iter().map(ImportFrom)) + .chain(eager_from_imports.into_iter().map( + |(import_from, _, comments, trailing_comma, aliases)| { + ImportFrom((import_from, comments, trailing_comma, aliases)) + }, + )) + .chain(lazy_straight_imports.into_iter().map(Import)) + .chain(lazy_from_imports.into_iter().map( + |(import_from, _, comments, trailing_comma, aliases)| { + ImportFrom((import_from, comments, trailing_comma, aliases)) + }, + )) .collect() } } diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_lazy_force_sort_within_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_lazy_force_sort_within_sections.py.snap new file mode 100644 index 0000000000000..264be2becd252 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_lazy_force_sort_within_sections.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> lazy_force_sort_within_sections.py:1:1 + | +1 | / lazy from math import pi +2 | | from math import pi +3 | | lazy import os +4 | | import os + | |_________^ + | +help: Organize imports +1 + from math import pi +2 + import os +3 | lazy from math import pi + - from math import pi +4 | lazy import os + - import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__from_first_lazy_from_first.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__from_first_lazy_from_first.py.snap new file mode 100644 index 0000000000000..82968eed9b4be --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__from_first_lazy_from_first.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> lazy_from_first.py:1:1 + | +1 | / lazy from math import pi +2 | | from math import pi +3 | | lazy import os +4 | | import os + | |_________^ + | +help: Organize imports +1 + from math import pi +2 + +3 + import os +4 | lazy from math import pi + - from math import pi +5 + +6 | lazy import os + - import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lazy_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lazy_imports.py.snap new file mode 100644 index 0000000000000..ddb91a231a38f --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lazy_imports.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> lazy_imports.py:1:1 + | +1 | / lazy from math import pi +2 | | from math import pi +3 | | lazy import os +4 | | import os + | |_________^ + | +help: Organize imports + - lazy from math import pi +1 + import os +2 | from math import pi +3 | lazy import os + - import os +4 + lazy from math import pi diff --git a/crates/ruff_linter/src/rules/isort/types.rs b/crates/ruff_linter/src/rules/isort/types.rs index 17b1567bcd63f..5fcdb618939f5 100644 --- a/crates/ruff_linter/src/rules/isort/types.rs +++ b/crates/ruff_linter/src/rules/isort/types.rs @@ -15,16 +15,19 @@ pub(crate) enum TrailingComma { pub(crate) struct ImportFromData<'a> { pub(crate) module: Option<&'a str>, pub(crate) level: u32, + pub(crate) is_lazy: bool, } #[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] pub(crate) struct AliasData<'a> { pub(crate) name: &'a str, pub(crate) asname: Option<&'a str>, + pub(crate) is_lazy: bool, } #[derive(Debug, Default, Clone)] pub(crate) struct ImportCommentSet<'a> { + pub(crate) first_index: Option, pub(crate) atop: Vec>, pub(crate) inline: Vec>, } @@ -72,6 +75,7 @@ impl<'a> Importable<'a> for ImportFromData<'a> { #[derive(Debug, Default)] pub(crate) struct ImportFromStatement<'a> { + pub(crate) first_index: Option, pub(crate) comments: ImportFromCommentSet<'a>, pub(crate) aliases: FxHashMap, ImportFromCommentSet<'a>>, pub(crate) trailing_comma: TrailingComma, From 0d84a0654a6b1f603bf44c535bf5bc9dfa3b50ea Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 8 Mar 2026 18:28:16 +0000 Subject: [PATCH 244/261] [ty] Add validation for type parameters with defaults after TypeVarTuple (#23807) --- crates/ruff_python_ast/src/nodes.rs | 9 + crates/ruff_python_codegen/src/generator.rs | 2 +- .../ruff_python_parser/src/semantic_errors.rs | 2 +- .../mdtest/generics/legacy/classes.md | 52 ++++++ .../mdtest/generics/pep695/aliases.md | 25 +++ .../mdtest/generics/pep695/classes.md | 39 ++++ ...ramet\342\200\246_(cd50ade911a6afa4).snap" | 102 +++++++++++ ...aramet\342\200\246_(6bb09b09c131074).snap" | 170 ++++++++++++++++++ .../src/types/infer/builder.rs | 9 + .../src/types/infer/deferred/mod.rs | 1 + .../src/types/infer/deferred/static_class.rs | 10 ++ .../infer/deferred/type_param_validation.rs | 84 +++++++++ 12 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/aliases.md_-_Generic_type_aliases\342\200\246_-_Default_type_paramet\342\200\246_(cd50ade911a6afa4).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" create mode 100644 crates/ty_python_semantic/src/types/infer/deferred/type_param_validation.rs diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index facef2f8f5625..00359a10ac6a5 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -3593,6 +3593,15 @@ impl Deref for TypeParams { } } +impl<'a> IntoIterator for &'a TypeParams { + type Item = &'a TypeParam; + type IntoIter = std::slice::Iter<'a, TypeParam>; + + fn into_iter(self) -> Self::IntoIter { + self.type_params.iter() + } +} + /// A suite represents a [Vec] of [Stmt]. /// /// See: diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index c03975348a1db..9a1492187b9bb 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -874,7 +874,7 @@ impl<'a> Generator<'a> { fn unparse_type_params(&mut self, type_params: &TypeParams) { self.p("["); let mut first = true; - for type_param in type_params.iter() { + for type_param in type_params { self.p_delim(&mut first, ", "); self.unparse_type_param(type_param); } diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index d6b1b2738af00..184a693324efd 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -716,7 +716,7 @@ impl SemanticSyntaxChecker { ctx: &Ctx, ) { let mut seen_default = false; - for type_param in type_params.iter() { + for type_param in type_params { let has_default = match type_param { ast::TypeParam::TypeVar(ast::TypeParamTypeVar { default, .. }) | ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { default, .. }) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index f1ce8d5eb277d..5b7aff47a3bbc 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -1063,5 +1063,57 @@ def func(x: D): ... func(G()) # error: [invalid-argument-type] ``` +## Default type parameter after `TypeVarTuple` + +A type parameter with a default cannot follow a `TypeVarTuple` in a legacy `Generic[...]` or +`Protocol[...]` subscription. This is prohibited by the typing spec because a `TypeVarTuple` +consumes all remaining positional type arguments, making any subsequent defaults meaningless. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import ParamSpec, TypeVar, TypeVarTuple, Unpack, Generic, Protocol + +T = TypeVar("T", default=int) +T2 = TypeVar("T2", default=str) +U = TypeVar("U") +Ts = TypeVarTuple("Ts") +Ts2 = TypeVarTuple("Ts2", default=Unpack[tuple[int, str]]) +Us = TypeVarTuple("Us") +P = ParamSpec("P", default=[int, str]) + +# TODO: should emit [invalid-type-variable-default] +class Foo(Generic[*Ts, T]): ... + +# TODO: should emit [invalid-type-variable-default] +class Bar(Generic[U, *Ts, T]): ... + +# TODO: should emit [invalid-type-variable-default] +class Baz(Protocol[*Ts, T]): ... + +# TODO: should emit [invalid-type-variable-default] +class Qux(Generic[*Ts, T, T2]): ... + +# TODO: should emit [invalid-type-variable-default] +class Quux(Generic[Unpack[Ts], T]): ... + +# Note: the spec says this is fine, +# but it raises `TypeError` at runtime +# () +# +# TODO: should emit [invalid-type-variable-default] +class Corge(Generic[Unpack[Us], P]): ... + +# TODO: should emit [invalid-type-variable-default] +class Grault(Generic[Unpack[Us], Unpack[Ts2]]): ... + +# These are fine: +class Ok1(Generic[U, *Ts]): ... +class Ok2(Generic[U, Unpack[Ts]]): ... +``` + [crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern [f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index ab1b6a0573d68..5c0cec90bb58e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -593,3 +593,28 @@ def j(x: Container1.Item, y: Container2.Item) -> None: # error: [invalid-assignment] "Object of type `mdtest_snippet.Container2.Item` is not assignable to `mdtest_snippet.Container1.Item`" a: Container1.Item = y ``` + +## Default type parameter after `TypeVarTuple` + + + +A type parameter with a default cannot follow a `TypeVarTuple` in a type parameter list. This is +prohibited by the typing spec because a `TypeVarTuple` consumes all remaining positional type +arguments, making any subsequent defaults meaningless. + +```py +# error: [invalid-type-variable-default] "Type parameter `T` with a default follows TypeVarTuple `Ts`" +type Alias1[*Ts, T = int] = tuple[*Ts, T] + +# error: [invalid-type-variable-default] +type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2] + +# error: [invalid-type-variable-default] +type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2] + +# error: [invalid-type-variable-default] +type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts] + +# These are fine: +type Ok1[T, *Ts] = tuple[T, *Ts] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 2eb4f6665994d..36bff77c0c54e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -932,5 +932,44 @@ def reveal_type(obj, /): ... reveal_type((1, 2, 3)) # revealed: tuple[Literal[1], Literal[2], Literal[3]] ``` +## Default type parameter after `TypeVarTuple` + + + +A type parameter with a default cannot follow a `TypeVarTuple` in a type parameter list. This is +prohibited by the typing spec because a `TypeVarTuple` consumes all remaining positional type +arguments, making any subsequent defaults meaningless. + +```py +# error: [invalid-type-variable-default] "Type parameter `T` with a default follows TypeVarTuple `Ts`" +class Foo[*Ts, T = int]: ... + +# error: [invalid-type-variable-default] +class Bar[T1, *Ts, T2 = int]: ... + +# error: [invalid-type-variable-default] +class Baz[*Ts, T1 = int, T2 = str]: ... + +# Note: the spec says this is fine, +# but it raises `TypeError` at runtime +# () +# +# error: [invalid-type-variable-default] +class Qux[*Ts, **P = [int, str]]: ... + +# error: [invalid-type-variable-default] +class Quux[*Ts, T1 = int, **P = [int, str]]: ... + +# error: [invalid-type-variable-default] +class Corge[*Ts, T1 = int, T2 = str, **P = [int, str]]: ... + +# error: [invalid-type-variable-default] +class Grault[*Us, *Ts = *tuple[int, str]]: ... + +# These are fine: +class Ok1[T, *Ts]: ... +class Ok3[*Ts]: ... +``` + [crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern [f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/aliases.md_-_Generic_type_aliases\342\200\246_-_Default_type_paramet\342\200\246_(cd50ade911a6afa4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/aliases.md_-_Generic_type_aliases\342\200\246_-_Default_type_paramet\342\200\246_(cd50ade911a6afa4).snap" new file mode 100644 index 0000000000000..8075121bda409 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/aliases.md_-_Generic_type_aliases\342\200\246_-_Default_type_paramet\342\200\246_(cd50ade911a6afa4).snap" @@ -0,0 +1,102 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 621 +expression: snapshot +--- + +--- +mdtest name: aliases.md - Generic type aliases: PEP 695 syntax - Default type parameter after `TypeVarTuple` +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | # error: [invalid-type-variable-default] "Type parameter `T` with a default follows TypeVarTuple `Ts`" + 2 | type Alias1[*Ts, T = int] = tuple[*Ts, T] + 3 | + 4 | # error: [invalid-type-variable-default] + 5 | type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2] + 6 | + 7 | # error: [invalid-type-variable-default] + 8 | type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2] + 9 | +10 | # error: [invalid-type-variable-default] +11 | type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts] +12 | +13 | # These are fine: +14 | type Ok1[T, *Ts] = tuple[T, *Ts] +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:2:13 + | +1 | # error: [invalid-type-variable-default] "Type parameter `T` with a default follows TypeVarTuple `Ts`" +2 | type Alias1[*Ts, T = int] = tuple[*Ts, T] + | --- ^^^^^^^ `T` has a default + | | + | `Ts` is a TypeVarTuple +3 | +4 | # error: [invalid-type-variable-default] + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:5:17 + | +4 | # error: [invalid-type-variable-default] +5 | type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2] + | --- ^^^^^^^^ `T2` has a default + | | + | `Ts` is a TypeVarTuple +6 | +7 | # error: [invalid-type-variable-default] + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:8:13 + | + 7 | # error: [invalid-type-variable-default] + 8 | type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2] + | --- ^^^^^^^^ -------- `T2` also has a default + | | | + | | `T1` has a default + | `Ts` is a TypeVarTuple + 9 | +10 | # error: [invalid-type-variable-default] + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:11:13 + | +10 | # error: [invalid-type-variable-default] +11 | type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts] + | --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` has a default + | | + | `Us` is a TypeVarTuple +12 | +13 | # These are fine: + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" new file mode 100644 index 0000000000000..f68d7abb74f9e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" @@ -0,0 +1,170 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: classes.md - Generic classes: PEP 695 syntax - Default type parameter after `TypeVarTuple` +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | # error: [invalid-type-variable-default] "Type parameter `T` with a default follows TypeVarTuple `Ts`" + 2 | class Foo[*Ts, T = int]: ... + 3 | + 4 | # error: [invalid-type-variable-default] + 5 | class Bar[T1, *Ts, T2 = int]: ... + 6 | + 7 | # error: [invalid-type-variable-default] + 8 | class Baz[*Ts, T1 = int, T2 = str]: ... + 9 | +10 | # Note: the spec says this is fine, +11 | # but it raises `TypeError` at runtime +12 | # () +13 | # +14 | # error: [invalid-type-variable-default] +15 | class Qux[*Ts, **P = [int, str]]: ... +16 | +17 | # error: [invalid-type-variable-default] +18 | class Quux[*Ts, T1 = int, **P = [int, str]]: ... +19 | +20 | # error: [invalid-type-variable-default] +21 | class Corge[*Ts, T1 = int, T2 = str, **P = [int, str]]: ... +22 | +23 | # error: [invalid-type-variable-default] +24 | class Grault[*Us, *Ts = *tuple[int, str]]: ... +25 | +26 | # These are fine: +27 | class Ok1[T, *Ts]: ... +28 | class Ok3[*Ts]: ... +``` + +# Diagnostics + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:2:11 + | +1 | # error: [invalid-type-variable-default] "Type parameter `T` with a default follows TypeVarTuple `Ts`" +2 | class Foo[*Ts, T = int]: ... + | --- ^^^^^^^ `T` has a default + | | + | `Ts` is a TypeVarTuple +3 | +4 | # error: [invalid-type-variable-default] + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:5:15 + | +4 | # error: [invalid-type-variable-default] +5 | class Bar[T1, *Ts, T2 = int]: ... + | --- ^^^^^^^^ `T2` has a default + | | + | `Ts` is a TypeVarTuple +6 | +7 | # error: [invalid-type-variable-default] + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:8:11 + | + 7 | # error: [invalid-type-variable-default] + 8 | class Baz[*Ts, T1 = int, T2 = str]: ... + | --- ^^^^^^^^ -------- `T2` also has a default + | | | + | | `T1` has a default + | `Ts` is a TypeVarTuple + 9 | +10 | # Note: the spec says this is fine, + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:15:11 + | +13 | # +14 | # error: [invalid-type-variable-default] +15 | class Qux[*Ts, **P = [int, str]]: ... + | --- ^^^^^^^^^^^^^^^^ `P` has a default + | | + | `Ts` is a TypeVarTuple +16 | +17 | # error: [invalid-type-variable-default] + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:18:12 + | +17 | # error: [invalid-type-variable-default] +18 | class Quux[*Ts, T1 = int, **P = [int, str]]: ... + | --- ^^^^^^^^ ---------------- `P` also has a default + | | | + | | `T1` has a default + | `Ts` is a TypeVarTuple +19 | +20 | # error: [invalid-type-variable-default] + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:21:13 + | +20 | # error: [invalid-type-variable-default] +21 | class Corge[*Ts, T1 = int, T2 = str, **P = [int, str]]: ... + | --- ^^^^^^^^ -------- ---------------- `P` also has a default + | | | | + | | | `T2` also has a default + | | `T1` has a default + | `Ts` is a TypeVarTuple +22 | +23 | # error: [invalid-type-variable-default] + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` + +``` +error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter + --> src/mdtest_snippet.py:24:14 + | +23 | # error: [invalid-type-variable-default] +24 | class Grault[*Us, *Ts = *tuple[int, str]]: ... + | --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` has a default + | | + | `Us` is a TypeVarTuple +25 | +26 | # These are fine: + | +info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple +info: rule `invalid-type-variable-default` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 29647007328c8..b57c6867c7fc2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1406,6 +1406,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { self.infer_expression(&type_alias.name, TypeContext::default()); + // Check that no type parameter with a default follows a TypeVarTuple + // in the type alias's PEP 695 type parameter list. + if let Some(type_params) = type_alias.type_params.as_deref() { + deferred::type_param_validation::check_no_default_after_typevar_tuple_pep695( + &self.context, + type_params, + ); + } + let rhs_scope = self .index .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) diff --git a/crates/ty_python_semantic/src/types/infer/deferred/mod.rs b/crates/ty_python_semantic/src/types/infer/deferred/mod.rs index f66ad455a2e25..09bc583400bdd 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/mod.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/mod.rs @@ -6,4 +6,5 @@ pub(super) mod final_variable; pub(super) mod function; pub(super) mod overloaded_function; pub(super) mod static_class; +pub(super) mod type_param_validation; pub(super) mod typeguard; diff --git a/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs b/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs index 16d9a79917508..8f29e86b9653e 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs @@ -753,6 +753,16 @@ pub(crate) fn check_static_class_definitions<'db>( } } + // Check that no type parameter with a default follows a TypeVarTuple. + // This is prohibited by the typing spec because a TypeVarTuple consumes + // all remaining positional type arguments. + if let Some(type_params) = class_node.type_params.as_deref() { + super::type_param_validation::check_no_default_after_typevar_tuple_pep695( + context, + type_params, + ); + } + if context.is_lint_enabled(&INVALID_GENERIC_CLASS) { if !class.has_pep_695_type_params(db) && let Some(generic_context) = class.legacy_generic_context(db) diff --git a/crates/ty_python_semantic/src/types/infer/deferred/type_param_validation.rs b/crates/ty_python_semantic/src/types/infer/deferred/type_param_validation.rs new file mode 100644 index 0000000000000..8ed101693a20b --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/deferred/type_param_validation.rs @@ -0,0 +1,84 @@ +use ruff_python_ast as ast; +use ruff_text_size::Ranged; + +use crate::diagnostic::format_enumeration; +use crate::types::{context::InferContext, diagnostic::INVALID_TYPE_VARIABLE_DEFAULT}; + +/// Check that no type parameter with a default follows a `TypeVarTuple` in a PEP 695 +/// type parameter list. This is prohibited by the typing spec because a `TypeVarTuple` +/// consumes all remaining positional type arguments. +/// +/// This check is used for both classes and type aliases with PEP 695 type parameters. +pub(crate) fn check_no_default_after_typevar_tuple_pep695( + context: &InferContext<'_, '_>, + type_params: &ast::TypeParams, +) { + let mut typevar_tuple: Option<&ast::TypeParamTypeVarTuple> = None; + let mut params_with_defaults = vec![]; + + for type_param in type_params { + if typevar_tuple.is_some() { + if type_param.default().is_some() { + params_with_defaults.push(type_param); + } + } else if let ast::TypeParam::TypeVarTuple(tvt) = type_param { + typevar_tuple = Some(tvt); + } + } + + let Some(typevar_tuple) = typevar_tuple else { + return; + }; + + if params_with_defaults.is_empty() { + return; + } + + let Some(builder) = + context.report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, params_with_defaults[0]) + else { + return; + }; + + let mut diagnostic = builder + .into_diagnostic("Type parameters with defaults cannot follow a TypeVarTuple parameter"); + + if let [single_param] = params_with_defaults.as_slice() { + let single_name = single_param.name(); + + diagnostic.set_concise_message(format_args!( + "Type parameter `{single_name}` with a default follows TypeVarTuple `{}`", + &typevar_tuple.name + )); + + diagnostic.set_primary_message(format_args!("`{single_name}` has a default")); + } else { + let names = format_enumeration(params_with_defaults.iter().map(|p| p.name())); + + diagnostic.set_concise_message(format_args!( + "Type parameters {names} with defaults follow TypeVarTuple `{}`", + &typevar_tuple.name + )); + + diagnostic.set_primary_message(format_args!( + "`{}` has a default", + params_with_defaults[0].name() + )); + + for param in ¶ms_with_defaults[1..] { + diagnostic.annotate( + context + .secondary(param.range()) + .message(format_args!("`{}` also has a default", param.name())), + ); + } + } + + diagnostic.annotate( + context + .secondary(typevar_tuple) + .message(format_args!("`{}` is a TypeVarTuple", &typevar_tuple.name)), + ); + + diagnostic.info("See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple"); +} From 880d19148737105a3954a8bd6d2996c4cfaebc6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:12:46 -0400 Subject: [PATCH 245/261] Update cargo-bins/cargo-binstall action to v1.17.6 (#23816) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4dd6abfa8a8ba..fe3335b3173c4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -471,7 +471,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo-binstall" - uses: cargo-bins/cargo-binstall@7691a5b29cda4f5499b819e5618012ba4b6c3334 # v1.17.5 + uses: cargo-bins/cargo-binstall@bc432b49369a3f25c8c8b19578a82060c18a5dd6 # v1.17.6 - name: "Install cargo-fuzz" # Download the latest version from quick install and not the github releases because github releases only has MUSL targets. run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm @@ -730,7 +730,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@7691a5b29cda4f5499b819e5618012ba4b6c3334 # v1.17.5 + - uses: cargo-bins/cargo-binstall@bc432b49369a3f25c8c8b19578a82060c18a5dd6 # v1.17.6 - run: cargo binstall --no-confirm cargo-shear - run: cargo shear From 5abba5f087510eca0262a3426a7bafbd48353a2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:12:53 -0400 Subject: [PATCH 246/261] Update dependency astral-sh/uv to v0.10.9 (#23817) --- .github/workflows/ci.yaml | 28 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/mypy_primer.yaml | 4 +-- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/sync_typeshed.yaml | 6 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- .github/workflows/ty-ecosystem-report.yaml | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe3335b3173c4..536dd5be9896e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -291,7 +291,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -350,7 +350,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -384,7 +384,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" enable-cache: "true" - name: "Run tests" run: | @@ -491,7 +491,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: shared-key: ruff-linux-debug @@ -528,7 +528,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -572,7 +572,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.10.7" + version: "0.10.9" - name: "Install Rust toolchain" run: rustup show @@ -684,7 +684,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -745,7 +745,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -798,7 +798,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 24 @@ -836,7 +836,7 @@ jobs: with: python-version: 3.13 activate-environment: true - version: "0.10.7" + version: "0.10.9" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -987,7 +987,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: "Install Rust toolchain" run: rustup show @@ -1068,7 +1068,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: "Install codspeed" uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 @@ -1119,7 +1119,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: "Install Rust toolchain" run: rustup show @@ -1163,7 +1163,7 @@ jobs: - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: "Install codspeed" uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index 77dae0e6532f5..3f01648d83fc8 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -36,7 +36,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 0d3f89dff29f3..83d7feaec7dce 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -54,7 +54,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: @@ -99,7 +99,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 36891e94189a4..30b23c299942a 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -24,7 +24,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: wheels-* diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 1d5b06dcbd06e..5697ac759c560 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -78,7 +78,7 @@ jobs: git config --global user.email '<>' - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -134,7 +134,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: Setup git run: | git config --global user.name typeshedbot @@ -175,7 +175,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: - version: "0.10.7" + version: "0.10.9" - name: Setup git run: | git config --global user.name typeshedbot diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index be2873309088a..da1d651ce85e0 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -41,7 +41,7 @@ jobs: uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: enable-cache: true - version: "0.10.7" + version: "0.10.9" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 3e23cd88d456d..a141ad323fca7 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -35,7 +35,7 @@ jobs: uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: enable-cache: true - version: "0.10.7" + version: "0.10.9" - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: From b6b861ca83352da47069054708009e37e06c8f3e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:13:14 -0400 Subject: [PATCH 247/261] Update dependency mkdocs-material to v9.7.3 (#23818) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index b2065045778bd..f9b094d8b05f3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ PyYAML==6.0.3 ruff==0.15.4 mkdocs==1.6.1 -mkdocs-material==9.7.2 +mkdocs-material==9.7.3 mkdocs-redirects==1.2.2 mdformat==1.0.0 mdformat-mkdocs==5.1.4 From 413d60570a59dc56c404d282e7d196668bab7a50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:13:23 -0400 Subject: [PATCH 248/261] Update dependency ruff to v0.15.5 (#23819) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index f9b094d8b05f3..e0bab9ae8b771 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.3 -ruff==0.15.4 +ruff==0.15.5 mkdocs==1.6.1 mkdocs-material==9.7.3 mkdocs-redirects==1.2.2 From f131f439abb3159e396cca156e566de0d56ba537 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:13:44 -0400 Subject: [PATCH 249/261] Update PyO3/maturin-action action to v1.50.1 (#23821) --- .github/workflows/build-binaries.yml | 16 ++++++++-------- .github/workflows/ci.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 176a37b2085b4..7bb62a907e8ee 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -49,7 +49,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build sdist" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: maturin-version: v1.11.5 command: sdist @@ -80,7 +80,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels - x86_64" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: maturin-version: v1.11.5 target: x86_64 @@ -123,7 +123,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels - aarch64" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: maturin-version: v1.11.5 target: aarch64 @@ -180,7 +180,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: maturin-version: v1.11.5 target: ${{ matrix.platform.target }} @@ -234,7 +234,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: maturin-version: v1.11.5 target: ${{ matrix.target }} @@ -315,7 +315,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: maturin-version: v1.11.5 target: ${{ matrix.platform.target }} @@ -382,7 +382,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: maturin-version: v1.11.5 target: ${{ matrix.target }} @@ -446,7 +446,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: maturin-version: v1.11.5 target: ${{ matrix.platform.target }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 536dd5be9896e..66fe239dafd07 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -777,7 +777,7 @@ jobs: - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.50.0 + uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.50.1 with: args: --out dist - name: "Test wheel" From 3d310e053506891a7fa1f190cef3cb6c31ab439d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:13:52 -0400 Subject: [PATCH 250/261] Update Rust crate jiff to v0.2.22 (#23822) --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 635b0b37bb0bf..f9c2bdaccbca2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,7 +710,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1169,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1853,9 +1853,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" +checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1863,14 +1863,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "jiff-static" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" +checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1" dependencies = [ "proc-macro2", "quote", @@ -3761,7 +3761,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4169,7 +4169,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5375,7 +5375,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] From 2538f69ef3738201871aa678e1e94e498906e355 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:14:01 -0400 Subject: [PATCH 251/261] Update Rust crate regex-syntax to v0.8.10 (#23823) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9c2bdaccbca2..ef4e6016c837b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3006,9 +3006,9 @@ checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ron" From f92156b2da7942062fdaa49df5a308ea0583139c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:41:45 -0400 Subject: [PATCH 252/261] Update Rust crate shellexpand to v3.1.2 (#23824) --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef4e6016c837b..91f8b9dc9d84f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,7 +580,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -701,7 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1077,7 +1077,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1790,7 +1790,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4007,9 +4007,9 @@ dependencies = [ [[package]] name = "shellexpand" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" dependencies = [ "dirs", ] From 275e28cce1c1de361ae26a2b17404f694ffc4ebc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:41:56 -0400 Subject: [PATCH 253/261] Update taiki-e/install-action action to v2.68.16 (#23825) --- .github/workflows/ci.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 66fe239dafd07..c543070af904e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -281,11 +281,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-insta - name: "Install uv" @@ -344,7 +344,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-nextest - name: "Install uv" @@ -378,7 +378,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-nextest - name: "Install uv" @@ -993,7 +993,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-codspeed @@ -1032,7 +1032,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-codspeed @@ -1071,7 +1071,7 @@ jobs: version: "0.10.9" - name: "Install codspeed" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-codspeed @@ -1125,7 +1125,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-codspeed @@ -1166,7 +1166,7 @@ jobs: version: "0.10.9" - name: "Install codspeed" - uses: taiki-e/install-action@cfdb446e391c69574ebc316dfb7d7849ec12b940 # v2.68.8 + uses: taiki-e/install-action@d6e286fa45544157a02d45a43742857ebbc25d12 # v2.68.16 with: tool: cargo-codspeed From 10c0e052aaccd1a67a1f933ad1453521d82d6990 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:42:10 -0400 Subject: [PATCH 254/261] Update CodSpeedHQ/action action to v4.11.0 (#23826) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c543070af904e..3af13ca73ac31 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1001,7 +1001,7 @@ jobs: run: cargo codspeed build --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser - name: "Run benchmarks" - uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f # v4.10.6 + uses: CodSpeedHQ/action@2ac572851726409c88c02a307f1ea2632a9ea59b # v4.11.0 with: mode: simulation run: cargo codspeed run @@ -1086,7 +1086,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f # v4.10.6 + uses: CodSpeedHQ/action@2ac572851726409c88c02a307f1ea2632a9ea59b # v4.11.0 with: mode: simulation run: cargo codspeed run --bench ty "${{ matrix.benchmark }}" @@ -1181,7 +1181,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f # v4.10.6 + uses: CodSpeedHQ/action@2ac572851726409c88c02a307f1ea2632a9ea59b # v4.11.0 env: # enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't # appear to provide much useful insight for our walltime benchmarks right now From 680b6f379ffe0947beddc6bc953ead554c6c7939 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:42:21 -0400 Subject: [PATCH 255/261] Update Rust crate serde_with to v3.17.0 (#23827) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91f8b9dc9d84f..78e0e8b1013a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3965,9 +3965,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "serde_core", "serde_with_macros", @@ -3975,9 +3975,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling", "proc-macro2", From 85bbb3a4e9c9caa61371c41019c33a6133ef816c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:42:38 -0400 Subject: [PATCH 256/261] Update actions/attest-build-provenance action to v4 (#23828) --- .github/workflows/build-docker.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 957f8d9554ad0..a719c1ab5cc24 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -174,7 +174,7 @@ jobs: echo "digest=${digest}" >> "$GITHUB_OUTPUT" - name: Generate artifact attestation - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ env.RUFF_BASE_IMG }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} @@ -279,7 +279,7 @@ jobs: annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ env.RUFF_BASE_IMG }} subject-digest: ${{ steps.build-and-push.outputs.digest }} @@ -358,7 +358,7 @@ jobs: echo "digest=${digest}" >> "$GITHUB_OUTPUT" - name: Generate artifact attestation - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ env.RUFF_BASE_IMG }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} From f7f5453138a8b8fe6e844888109dfc4af7488dcf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:43:10 -0400 Subject: [PATCH 257/261] Update prek dependencies (#23820) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63152168f1b5a..94fdf2571d953 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: priority: 0 - repo: https://github.com/crate-ci/typos - rev: v1.43.5 + rev: v1.44.0 hooks: - id: typos priority: 0 @@ -66,7 +66,7 @@ repos: priority: 0 - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.2 + rev: 0.37.0 hooks: - id: check-github-workflows priority: 0 @@ -93,7 +93,7 @@ repos: priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.2 + rev: v0.15.4 hooks: - id: ruff-format priority: 0 @@ -117,7 +117,7 @@ repos: # Priority 2: ruffen-docs runs after markdownlint-fix (both modify markdown). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.2 + rev: v0.15.4 hooks: - id: ruff-format name: mdtest format From 5c863c927e3a8fc87a1373423f94cc85093da071 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:04:11 +0000 Subject: [PATCH 258/261] Update Artifact GitHub Actions dependencies (#23829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/download-artifact](https://redirect.github.com/actions/download-artifact) | action | major | `v7.0.0` → `v8.0.0` | | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | major | `v6.0.0` → `v7.0.0` | --- ### Release Notes
actions/download-artifact (actions/download-artifact) ### [`v8.0.0`](https://redirect.github.com/actions/download-artifact/releases/tag/v8.0.0) [Compare Source](https://redirect.github.com/actions/download-artifact/compare/v7.0.0...v8.0.0) ##### v8 - What's new ##### Direct downloads To support direct uploads in `actions/upload-artifact`, the action will no longer attempt to unzip all downloaded files. Instead, the action checks the `Content-Type` header ahead of unzipping and skips non-zipped files. Callers wishing to download a zipped file as-is can also set the new `skip-decompress` parameter to `false`. ##### Enforced checks (breaking) A previous release introduced digest checks on the download. If a download hash didn't match the expected hash from the server, the action would log a warning. Callers can now configure the behavior on mismatch with the `digest-mismatch` parameter. To be secure by default, we are now defaulting the behavior to `error` which will fail the workflow run. ##### ESM To support new versions of the @​actions/\* packages, we've upgraded the package to ESM. ##### What's Changed - Don't attempt to un-zip non-zipped downloads by [@​danwkennedy](https://redirect.github.com/danwkennedy) in [#​460](https://redirect.github.com/actions/download-artifact/pull/460) - Add a setting to specify what to do on hash mismatch and default it to `error` by [@​danwkennedy](https://redirect.github.com/danwkennedy) in [#​461](https://redirect.github.com/actions/download-artifact/pull/461) **Full Changelog**:
actions/upload-artifact (actions/upload-artifact) ### [`v7.0.0`](https://redirect.github.com/actions/upload-artifact/releases/tag/v7.0.0) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v6.0.0...v7.0.0) #### v7 What's new ##### Direct Uploads Adds support for uploading single files directly (unzipped). Callers can set the new `archive` parameter to `false` to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The `name` parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file. ##### ESM To support new versions of the `@actions/*` packages, we've upgraded the package to ESM. #### What's Changed - Add proxy integration test by [@​Link-](https://redirect.github.com/Link-) in [#​754](https://redirect.github.com/actions/upload-artifact/pull/754) - Upgrade the module to ESM and bump dependencies by [@​danwkennedy](https://redirect.github.com/danwkennedy) in [#​762](https://redirect.github.com/actions/upload-artifact/pull/762) - Support direct file uploads by [@​danwkennedy](https://redirect.github.com/danwkennedy) in [#​764](https://redirect.github.com/actions/upload-artifact/pull/764) #### New Contributors - [@​Link-](https://redirect.github.com/Link-) made their first contribution in [#​754](https://redirect.github.com/actions/upload-artifact/pull/754) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-binaries.yml | 30 ++++++++++---------- .github/workflows/build-docker.yml | 6 ++-- .github/workflows/build-wasm.yml | 2 +- .github/workflows/ci.yaml | 10 +++---- .github/workflows/memory_report.yaml | 2 +- .github/workflows/mypy_primer.yaml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/publish-wasm.yml | 2 +- .github/workflows/ty-ecosystem-analyzer.yaml | 8 +++--- .github/workflows/ty-ecosystem-report.yaml | 2 +- .github/workflows/typing_conformance.yaml | 2 +- 11 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 7bb62a907e8ee..a56abad94733f 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -60,7 +60,7 @@ jobs: "${MODULE_NAME}" --help python -m "${MODULE_NAME}" --help - name: "Upload sdist" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-sdist path: dist @@ -86,7 +86,7 @@ jobs: target: x86_64 args: --release --locked --out dist --compatibility pypi - name: "Upload wheels" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-macos-x86_64 path: dist @@ -101,7 +101,7 @@ jobs: tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-macos-x86_64 path: | @@ -134,7 +134,7 @@ jobs: ruff --help python -m ruff --help - name: "Upload wheels" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-aarch64-apple-darwin path: dist @@ -149,7 +149,7 @@ jobs: tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-aarch64-apple-darwin path: | @@ -196,7 +196,7 @@ jobs: "${MODULE_NAME}" --help python -m "${MODULE_NAME}" --help - name: "Upload wheels" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-${{ matrix.platform.target }} path: dist @@ -207,7 +207,7 @@ jobs: 7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-${{ matrix.platform.target }} path: | @@ -247,7 +247,7 @@ jobs: "${MODULE_NAME}" --help python -m "${MODULE_NAME}" --help - name: "Upload wheels" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-${{ matrix.target }} path: dist @@ -265,7 +265,7 @@ jobs: tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-${{ matrix.target }} path: | @@ -337,7 +337,7 @@ jobs: pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall ruff --help - name: "Upload wheels" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-${{ matrix.platform.target }} path: dist @@ -355,7 +355,7 @@ jobs: tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-${{ matrix.platform.target }} path: | @@ -398,7 +398,7 @@ jobs: .venv/bin/${MODULE_NAME} --help; " - name: "Upload wheels" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-${{ matrix.target }} path: dist @@ -416,7 +416,7 @@ jobs: tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-${{ matrix.target }} path: | @@ -466,7 +466,7 @@ jobs: .venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall .venv/bin/${{ env.MODULE_NAME }} --help - name: "Upload wheels" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-${{ matrix.platform.target }} path: dist @@ -484,7 +484,7 @@ jobs: tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-${{ matrix.platform.target }} path: | diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index a719c1ab5cc24..73745d90b5bc6 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -102,7 +102,7 @@ jobs: touch "/tmp/digests/${digest#sha256:}" - name: Upload digests - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: digests-${{ env.PLATFORM_TUPLE }} path: /tmp/digests/* @@ -123,7 +123,7 @@ jobs: packages: write steps: - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: path: /tmp/digests pattern: digests-* @@ -301,7 +301,7 @@ jobs: packages: write steps: - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: path: /tmp/digests pattern: digests-* diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index 603e61e7e80ae..f5358c5d05978 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -52,7 +52,7 @@ jobs: mv /tmp/package.json crates/ruff_wasm/pkg - run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg - name: "Upload wasm artifact" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: # Avoid prefixing the name with `artifacts-` here to exclude it from the GitHub release. name: wasm-npm-${{ matrix.target }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3af13ca73ac31..0439b3d54d7f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -656,7 +656,7 @@ jobs: # NOTE: astral-sh-bot uses this artifact to post comments on PRs. # Make sure to update the bot if you rename the artifact. - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 name: Upload Results with: name: ecosystem-result @@ -1040,7 +1040,7 @@ jobs: run: cargo codspeed build -m instrumentation --features "codspeed,ty_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty - name: "Upload benchmark binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: benchmarks-instrumented-ty-binary path: target/codspeed @@ -1076,7 +1076,7 @@ jobs: tool: cargo-codspeed - name: "Download benchmark binary" - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: benchmarks-instrumented-ty-binary path: target/codspeed @@ -1133,7 +1133,7 @@ jobs: run: cargo codspeed build -m walltime --features "codspeed,ty_walltime" --profile profiling --no-default-features -p ruff_benchmark - name: "Upload benchmark binary" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: benchmarks-walltime-binary path: target/codspeed @@ -1171,7 +1171,7 @@ jobs: tool: cargo-codspeed - name: "Download benchmark binary" - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: benchmarks-walltime-binary path: target/codspeed diff --git a/.github/workflows/memory_report.yaml b/.github/workflows/memory_report.yaml index 34263d94a8451..438a28e8677b8 100644 --- a/.github/workflows/memory_report.yaml +++ b/.github/workflows/memory_report.yaml @@ -98,7 +98,7 @@ jobs: # NOTE: astral-sh-bot uses this artifact to post comments on PRs. # Make sure to update the bot if you rename the artifact. - name: Upload diff - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: memory_report_diff path: memory_report_diff.diff diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 83d7feaec7dce..60d82f50f54e9 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -76,7 +76,7 @@ jobs: # NOTE: astral-sh-bot uses this artifact to post comments on PRs. # Make sure to update the bot if you rename the artifact. - name: Upload diff - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: mypy_primer_diff path: mypy_primer.diff diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 30b23c299942a..5a20e9690cb56 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -25,7 +25,7 @@ jobs: uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1 with: version: "0.10.9" - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: pattern: wheels-* path: wheels diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index 24aa8f799d2a4..ed92bf719b24e 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -22,7 +22,7 @@ jobs: target: [web, bundler, nodejs] fail-fast: false steps: - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: wasm-npm-${{ matrix.target }} path: pkg diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index da1d651ce85e0..fed08c3a9dc0f 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -124,7 +124,7 @@ jobs: # NOTE: astral-sh-bot uses this artifact to post comments on PRs. # Make sure to update the bot if you rename the artifact. - name: "Upload full report" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: full-report path: dist/ @@ -132,19 +132,19 @@ jobs: # NOTE: astral-sh-bot uses this artifact to post comments on PRs. # Make sure to update the bot if you rename the artifact. - name: Upload comment - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: comment.md path: comment.md - name: Upload diagnostics diff - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: diff.html path: dist/diff.html - name: Upload timing diff - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: timing.html path: dist/timing.html diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index a141ad323fca7..a5218bcf315cc 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -78,7 +78,7 @@ jobs: # NOTE: astral-sh-bot uses this artifact to publish the ecosystem report. # Make sure to update the bot if you rename the artifact. - name: "Upload ecosystem report" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: full-report path: dist/ diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index 6613652aabea3..3145d29425ec2 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -103,7 +103,7 @@ jobs: # NOTE: astral-sh-bot uses this artifact to post comments on PRs. # Make sure to update the bot if you rename the artifact. - name: Upload diff - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: typing_conformance_diagnostics_diff path: typing_conformance_diagnostics.diff From 92a77f2a69639b63306534488355e18de2dd081a Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 9 Mar 2026 08:28:39 +0000 Subject: [PATCH 259/261] [ty] Simplify `which` integration (#23690) --- Cargo.lock | 11 -- Cargo.toml | 1 - crates/ruff/src/commands/analyze_graph.rs | 4 +- crates/ruff_db/Cargo.toml | 7 +- crates/ruff_db/src/system.rs | 40 ++++++- crates/ruff_db/src/system/os.rs | 38 +++---- crates/ruff_db/src/system/test.rs | 23 ++-- crates/ruff_db/src/system/which.rs | 123 ---------------------- crates/ruff_graph/Cargo.toml | 2 +- crates/ruff_graph/src/db.rs | 18 ++-- crates/ty_server/src/system.rs | 7 +- crates/ty_site_packages/Cargo.toml | 3 - crates/ty_site_packages/src/lib.rs | 15 +-- crates/ty_test/src/db.rs | 6 +- crates/ty_wasm/src/lib.rs | 8 +- 15 files changed, 87 insertions(+), 219 deletions(-) delete mode 100644 crates/ruff_db/src/system/which.rs diff --git a/Cargo.lock b/Cargo.lock index 78e0e8b1013a4..38d4379870be7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1803,15 +1803,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is_executable" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3154,7 +3145,6 @@ dependencies = [ "glob", "ignore", "insta", - "is_executable", "matchit", "path-slash", "pathdiff", @@ -4828,7 +4818,6 @@ dependencies = [ "strum_macros", "tracing", "ty_static", - "which", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1a7e482d661d4..80977dcd7eb81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,6 @@ insta = { version = "1.35.1" } insta-cmd = { version = "0.6.0" } is-macro = { version = "0.3.5" } is-wsl = { version = "0.4.0" } -is_executable = { version = "1.0.5" } itertools = { version = "0.14.0" } jiff = { version = "0.2.0" } jod-thread = { version = "1.0.0" } diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index 968dac4c355a2..48fdf14a7b15e 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -5,7 +5,7 @@ use anyhow::Result; use indexmap::IndexSet; use log::{debug, warn}; use path_absolutize::CWD; -use ruff_db::system::{SystemPath, SystemPathBuf}; +use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports}; use ruff_linter::package::PackageRoot; use ruff_linter::source_kind::SourceKind; @@ -95,7 +95,9 @@ pub(crate) fn analyze_graph( .filter_map(|path| SystemPathBuf::from_path_buf(path.to_path_buf()).ok()), ); + let system = OsSystem::default(); let db = ModuleDb::from_src_roots( + system, src_roots.into_iter().collect(), pyproject_config .settings diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index 74007adb3fc85..f3db4a9673f09 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -32,7 +32,6 @@ filetime = { workspace = true } get-size2 = { workspace = true } glob = { workspace = true } ignore = { workspace = true, optional = true } -is_executable = { workspace = true } matchit = { workspace = true } path-slash = { workspace = true } pathdiff = { workspace = true } @@ -47,6 +46,7 @@ supports-hyperlinks = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, optional = true } +which = { workspace = true, optional = true } zip = { workspace = true } [target.'cfg(not(target_arch="wasm32"))'.dependencies] @@ -55,9 +55,6 @@ etcetera = { workspace = true, optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] web-time = { version = "1.1.0" } -[target.'cfg(not(target_family = "wasm"))'.dependencies] -which = { workspace = true } - [dev-dependencies] insta = { workspace = true, features = ["filters"] } tempfile = { workspace = true } @@ -65,7 +62,7 @@ tempfile = { workspace = true } [features] cache = ["ruff_cache"] junit = ["dep:quick-junit"] -os = ["ignore", "dep:etcetera"] +os = ["ignore", "dep:etcetera", "dep:which"] serde = [ "camino/serde1", "dep:serde", diff --git a/crates/ruff_db/src/system.rs b/crates/ruff_db/src/system.rs index d2de5a5959584..9bd34c7432502 100644 --- a/crates/ruff_db/src/system.rs +++ b/crates/ruff_db/src/system.rs @@ -29,10 +29,9 @@ mod os; mod path; mod test; pub mod walk_directory; -#[cfg(not(target_family = "wasm"))] -mod which; pub type Result = std::io::Result; +pub type WhichResult = std::result::Result; /// The system on which Ruff runs. /// @@ -97,6 +96,9 @@ pub trait System: Debug + Sync + Send { None } + /// Find an executable binary's path by name. + fn which(&self, binary_name: &str) -> WhichResult; + /// Reads the content of the file at `path` into a [`String`]. fn read_to_string(&self, path: &SystemPath) -> Result; @@ -147,9 +149,6 @@ pub trait System: Debug + Sync + Send { .is_ok_and(|metadata| metadata.file_type.is_file()) } - /// Returns `true` if `path` exists and is marked as executable. - fn is_executable(&self, path: &SystemPath) -> bool; - /// Returns the current working directory fn current_directory(&self) -> &SystemPath; @@ -478,3 +477,34 @@ pub fn file_time_now() -> FileTime { FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos) }) } + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum WhichError { + /// An executable binary with that name was not found + CannotFindBinaryPath, + + /// There was nowhere to search and the provided name wasn't an absolute path + CannotGetCurrentDirAndPathListEmpty, + + /// Failed to canonicalize the path found + CannotCanonicalize, + + /// The executable exists but its path contains non UTF8 characters. + NonUtf8Path, +} + +impl Error for WhichError {} + +impl fmt::Display for WhichError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WhichError::CannotFindBinaryPath => write!(f, "cannot find binary path"), + WhichError::CannotGetCurrentDirAndPathListEmpty => write!( + f, + "no path to search and provided name is not an absolute path" + ), + WhichError::CannotCanonicalize => write!(f, "cannot canonicalize path"), + WhichError::NonUtf8Path => write!(f, "non UTF-8 path"), + } + } +} diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index 6c0e21b2e7b25..f39fe7f0dccc1 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -7,7 +7,7 @@ use super::walk_directory::{ use crate::max_parallelism; use crate::system::{ CaseSensitivity, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result, System, - SystemPath, SystemPathBuf, SystemVirtualPath, WritableSystem, + SystemPath, SystemPathBuf, SystemVirtualPath, WhichError, WhichResult, WritableSystem, }; use filetime::FileTime; use ruff_notebook::{Notebook, NotebookError}; @@ -129,15 +129,25 @@ impl System for OsSystem { self.inner.case_sensitivity } - fn is_executable(&self, path: &SystemPath) -> bool { - is_executable::is_executable(path.as_std_path()) + fn which(&self, name: &str) -> WhichResult { + let path = which::which(name).map_err(|err| match err { + which::Error::CannotFindBinaryPath => WhichError::CannotFindBinaryPath, + which::Error::CannotGetCurrentDirAndPathListEmpty => { + WhichError::CannotGetCurrentDirAndPathListEmpty + } + which::Error::CannotCanonicalize => WhichError::CannotCanonicalize, + })?; + + match SystemPathBuf::from_path_buf(path) { + Ok(path) => Ok(path), + Err(_) => Err(WhichError::NonUtf8Path), + } } fn current_directory(&self) -> &SystemPath { &self.inner.cwd } - #[cfg(not(target_arch = "wasm32"))] fn user_config_directory(&self) -> Option { // In testing, we allow overriding the user configuration directory by using a // thread local because overriding the environment variables breaks test isolation @@ -154,23 +164,10 @@ impl System for OsSystem { SystemPathBuf::from_path_buf(strategy.config_dir()).ok() } - // TODO: Remove this feature gating once `ruff_wasm` no longer indirectly depends on `ruff_db` with the - // `os` feature enabled (via `ruff_workspace` -> `ruff_graph` -> `ruff_db`). - #[cfg(target_arch = "wasm32")] - fn user_config_directory(&self) -> Option { - #[cfg(feature = "testing")] - if let Ok(directory_override) = self.try_get_user_config_directory_override() { - return directory_override; - } - - None - } - /// Returns an absolute cache directory on the system. /// /// On Linux and macOS, uses `$XDG_CACHE_HOME/ty` or `.cache/ty`. /// On Windows, uses `C:\Users\User\AppData\Local\ty\cache`. - #[cfg(not(target_arch = "wasm32"))] fn cache_dir(&self) -> Option { use etcetera::BaseStrategy as _; @@ -192,13 +189,6 @@ impl System for OsSystem { Some(cache_dir) } - // TODO: Remove this feature gating once `ruff_wasm` no longer indirectly depends on `ruff_db` with the - // `os` feature enabled (via `ruff_workspace` -> `ruff_graph` -> `ruff_db`). - #[cfg(target_arch = "wasm32")] - fn cache_dir(&self) -> Option { - None - } - /// Creates a builder to recursively walk `path`. /// /// The walker ignores files according to [`ignore::WalkBuilder::standard_filters`] diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs index f63fc2115d540..d43fa5570703e 100644 --- a/crates/ruff_db/src/system/test.rs +++ b/crates/ruff_db/src/system/test.rs @@ -8,7 +8,7 @@ use crate::Db; use crate::files::File; use crate::system::{ CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, Result, System, - SystemPath, SystemPathBuf, SystemVirtualPath, + SystemPath, SystemPathBuf, SystemVirtualPath, WhichError, WhichResult, }; use super::WritableSystem; @@ -121,10 +121,6 @@ impl System for TestSystem { self.system().read_virtual_path_to_notebook(path) } - fn is_executable(&self, path: &SystemPath) -> bool { - self.system().is_executable(path) - } - fn current_directory(&self) -> &SystemPath { self.system().current_directory() } @@ -137,6 +133,10 @@ impl System for TestSystem { self.system().cache_dir() } + fn which(&self, _name: &str) -> WhichResult { + Err(WhichError::CannotFindBinaryPath) + } + fn read_directory<'a>( &'a self, path: &SystemPath, @@ -197,10 +197,7 @@ impl System for TestSystem { impl Default for TestSystem { fn default() -> Self { - Self { - inner: Arc::new(InMemorySystem::default()), - env_overrides: Arc::new(Mutex::new(FxHashMap::default())), - } + Self::new(InMemorySystem::default()) } } @@ -395,10 +392,6 @@ impl System for InMemorySystem { Notebook::from_source_code(&content) } - fn is_executable(&self, path: &SystemPath) -> bool { - self.memory_fs.is_executable(path) - } - fn current_directory(&self) -> &SystemPath { self.memory_fs.current_directory() } @@ -411,6 +404,10 @@ impl System for InMemorySystem { None } + fn which(&self, _name: &str) -> WhichResult { + Err(WhichError::CannotFindBinaryPath) + } + fn read_directory<'a>( &'a self, path: &SystemPath, diff --git a/crates/ruff_db/src/system/which.rs b/crates/ruff_db/src/system/which.rs deleted file mode 100644 index 749f0967d77e1..0000000000000 --- a/crates/ruff_db/src/system/which.rs +++ /dev/null @@ -1,123 +0,0 @@ -/*! -Provides a trait implementation for `which::Sys` based on `System`. - -This lets us use the `which` crate to discover executables in `PATH` -in a way that doesn't break out of our `System` abstraction. -*/ - -use std::{ - env::VarError, - ffi::{OsStr, OsString}, - io, - path::{Path, PathBuf}, -}; - -use which::sys::{Sys, SysMetadata, SysReadDirEntry}; - -use super::{DirectoryEntry, Metadata, System, SystemPath}; - -impl Sys for &'_ dyn System { - type ReadDirEntry = DirectoryEntry; - - type Metadata = Metadata; - - fn is_windows(&self) -> bool { - cfg!(windows) - } - - fn current_dir(&self) -> io::Result { - Ok(self.current_directory().as_std_path().to_owned()) - } - - fn home_dir(&self) -> Option { - #[cfg(windows)] - const NAME: &str = "USERPROFILE"; - #[cfg(not(windows))] - const NAME: &str = "HOME"; - env_var_os(*self, NAME).map(PathBuf::from) - } - - fn env_split_paths(&self, paths: &OsStr) -> Vec { - std::env::split_paths(paths).collect() - } - - fn env_path(&self) -> Option { - env_var_os(*self, "PATH") - } - - fn env_path_ext(&self) -> Option { - env_var_os(*self, "PATHEXT") - } - - fn metadata(&self, path: &Path) -> io::Result { - self.path_metadata(system_path_from_std_path(path)?) - } - - fn symlink_metadata(&self, path: &Path) -> io::Result { - // N.B. Our `System` abstraction doesn't seem to know about - // symlinks, so it isn't really possible to implement - // symlink-only metadata here. - // - // Thankfully, the `which` crate only uses this in one place - // as of 2026-02-10. It's used to support reparse points on - // Windows. I think this is somewhat obscure, so we mush ahead - // without it. We can reconsider how we implement this if this - // becomes a problem. ---AG - self.metadata(path) - } - - fn read_dir( - &self, - path: &Path, - ) -> io::Result>>> { - let iter = self - .read_directory(system_path_from_std_path(path)?)? - .collect::>() - .into_iter(); - Ok(Box::new(iter)) - } - - fn is_valid_executable(&self, path: &Path) -> io::Result { - Ok(self.is_executable(system_path_from_std_path(path)?)) - } -} - -impl SysReadDirEntry for DirectoryEntry { - fn file_name(&self) -> OsString { - // DirectoryEntry should always have a file name - self.path.file_name().unwrap().into() - } - - fn path(&self) -> PathBuf { - self.path.clone().into_std_path_buf() - } -} - -impl SysMetadata for Metadata { - fn is_symlink(&self) -> bool { - self.file_type.is_symlink() - } - - fn is_file(&self) -> bool { - self.file_type.is_file() - } -} - -fn env_var_os(system: &dyn System, name: &str) -> Option { - system.env_var(name).map_or_else( - |e| match e { - VarError::NotPresent => None, - VarError::NotUnicode(path) => Some(path), - }, - |x| Some(x.into()), - ) -} - -fn system_path_from_std_path(path: &Path) -> io::Result<&SystemPath> { - SystemPath::from_std_path(path).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidFilename, - format!("invalid UTF-8: {}", path.display()), - ) - }) -} diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index a48c7a3ca443d..d4f2de5d215d7 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -15,7 +15,7 @@ ignored = ["ruff_cache"] [dependencies] ruff_cache = { workspace = true } -ruff_db = { workspace = true, features = ["os", "serde"] } +ruff_db = { workspace = true, features = ["serde"] } ruff_linter = { workspace = true } ruff_macros = { workspace = true } ruff_python_ast = { workspace = true } diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index 97cb28a121eac..6d92772401c88 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -1,10 +1,11 @@ use anyhow::{Context, Result}; +use std::panic::RefUnwindSafe; use std::sync::Arc; use zip::CompressionMethod; use ruff_db::Db as SourceDb; use ruff_db::files::Files; -use ruff_db::system::{OsSystem, System, SystemPathBuf}; +use ruff_db::system::{System, SystemPathBuf}; use ruff_db::vendored::{VendoredFileSystem, VendoredFileSystemBuilder}; use ruff_python_ast::PythonVersion; use ty_module_resolver::{SearchPathSettings, SearchPaths}; @@ -21,19 +22,22 @@ static EMPTY_VENDORED: std::sync::LazyLock = std::sync::Lazy pub struct ModuleDb { storage: salsa::Storage, files: Files, - system: OsSystem, + system: Arc, search_paths: Arc, python_version: PythonVersion, } impl ModuleDb { /// Initialize a [`ModuleDb`] from the given source root. - pub fn from_src_roots( + pub fn from_src_roots( + system: S, src_roots: Vec, python_version: PythonVersion, venv_path: Option, - ) -> Result { - let system = OsSystem::default(); + ) -> Result + where + S: System + 'static + Send + Sync + RefUnwindSafe, + { let mut search_path_settings = SearchPathSettings::new(src_roots); // TODO: Consider calling `PythonEnvironment::discover` if the `venv_path` is not provided. if let Some(venv_path) = venv_path { @@ -51,7 +55,7 @@ impl ModuleDb { let db = Self { storage: salsa::Storage::new(None), files: Files::default(), - system, + system: Arc::new(system), search_paths: Arc::new(search_paths), python_version, }; @@ -70,7 +74,7 @@ impl SourceDb for ModuleDb { } fn system(&self) -> &dyn System { - &self.system + &*self.system } fn files(&self) -> &Files { diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index 0b6a9c25348e5..325c195c9b1f0 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -14,7 +14,8 @@ use ruff_db::files::{File, FilePath}; use ruff_db::system::walk_directory::WalkDirectoryBuilder; use ruff_db::system::{ CaseSensitivity, DirectoryEntry, FileType, GlobError, Metadata, PatternError, Result, System, - SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, WritableSystem, + SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, WhichResult, + WritableSystem, }; use ruff_notebook::{Notebook, NotebookError}; use ruff_python_ast::PySourceType; @@ -228,8 +229,8 @@ impl System for LSPSystem { } } - fn is_executable(&self, path: &SystemPath) -> bool { - self.native_system.is_executable(path) + fn which(&self, name: &str) -> WhichResult { + self.native_system.which(name) } fn current_directory(&self) -> &SystemPath { diff --git a/crates/ty_site_packages/Cargo.toml b/crates/ty_site_packages/Cargo.toml index a5dee608b5e9f..1c3451c5e3cd0 100644 --- a/crates/ty_site_packages/Cargo.toml +++ b/crates/ty_site_packages/Cargo.toml @@ -27,9 +27,6 @@ strum = { workspace = true } strum_macros = { workspace = true } tracing = { workspace = true } -[target.'cfg(not(target_family = "wasm"))'.dependencies] -which = { workspace = true } - [dev-dependencies] ruff_db = { workspace = true, features = ["testing", "os"] } diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index fcb775aa95219..932d797e00fa7 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -728,24 +728,11 @@ pub(crate) fn conda_environment_from_env( Some(path) } -#[cfg(target_family = "wasm")] -pub(crate) fn environment_from_binary( - _system: &dyn System, - _binary: &str, -) -> Option { - None -} - -#[cfg(not(target_family = "wasm"))] pub(crate) fn environment_from_binary( system: &dyn System, binary: &str, ) -> Option { - let binary = which::WhichConfig::new_with_sys(system) - .binary_name(binary.into()) - .first_result() - .ok()?; - let binary = SystemPathBuf::from_path_buf(binary).ok()?; + let binary = system.which(binary).ok()?; let env = PythonEnvironment::new(binary, SysPrefixPathOrigin::PythonBinary, system).ok()?; // TODO: replace this with better shim support, e.g. pyenv diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index 968f5e706476d..e98a93648318f 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -4,7 +4,7 @@ use ruff_db::diagnostic::Severity; use ruff_db::files::{File, Files}; use ruff_db::system::{ CaseSensitivity, DbWithWritableSystem, InMemorySystem, OsSystem, System, SystemPath, - SystemPathBuf, WritableSystem, + SystemPathBuf, WhichResult, WritableSystem, }; use ruff_db::vendored::VendoredFileSystem; use ruff_notebook::{Notebook, NotebookError}; @@ -312,8 +312,8 @@ impl System for MdtestSystem { self.as_system().case_sensitivity() } - fn is_executable(&self, path: &SystemPath) -> bool { - self.as_system().is_executable(path) + fn which(&self, name: &str) -> WhichResult { + self.as_system().which(name) } fn current_directory(&self) -> &SystemPath { diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index c6e6d736b4ef9..d5477a1c04dd3 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -8,7 +8,7 @@ use ruff_db::source::{SourceText, line_index, source_text}; use ruff_db::system::walk_directory::WalkDirectoryBuilder; use ruff_db::system::{ CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, PatternError, System, - SystemPath, SystemPathBuf, SystemVirtualPath, WritableSystem, + SystemPath, SystemPathBuf, SystemVirtualPath, WhichError, WhichResult, WritableSystem, }; use ruff_db::vendored::VendoredPath; use ruff_diagnostics::{Applicability, Edit}; @@ -1402,10 +1402,8 @@ impl System for WasmSystem { CaseSensitivity::CaseSensitive } - fn is_executable(&self, path: &SystemPath) -> bool { - // Since permissions of all files is 755, - // it follows that every file is executable. - self.is_file(path) + fn which(&self, _name: &str) -> WhichResult { + Err(WhichError::CannotFindBinaryPath) } fn current_directory(&self) -> &SystemPath { From a162c197e74cd914396042c99242e21b3094ec3e Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Mon, 9 Mar 2026 10:09:39 +0100 Subject: [PATCH 260/261] Add widen_ty_visibility.sh script This branch (ty-types-2) is a fork of astral-sh/ruff that widens visibility in ty_python_semantic from pub(crate)/pub(super) to pub for consumption by OpenRewrite. To sync with upstream: scripts/widen_ty_visibility.sh To sync and run tests: scripts/widen_ty_visibility.sh --test --- scripts/CLAUDE.md | 31 ++++++++ scripts/widen_ty_visibility.sh | 139 +++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 scripts/CLAUDE.md create mode 100755 scripts/widen_ty_visibility.sh diff --git a/scripts/CLAUDE.md b/scripts/CLAUDE.md new file mode 100644 index 0000000000000..fe60cce64a7f3 --- /dev/null +++ b/scripts/CLAUDE.md @@ -0,0 +1,31 @@ +# Fork Maintenance: ty-types-2 + +This branch keeps two custom commits on top of `upstream/main`: + +1. **Script commit** — `Add widen_ty_visibility.sh script` + Contains this file and the sync script. + +2. **Visibility commit** — `Widen ty_python_semantic visibility to pub` + Blanket-widens all `pub(crate)` / `pub(super)` to `pub` in `ty_python_semantic`, + plus targeted fix-ups for items the blanket sed misses. + +## Syncing with upstream + +```sh +scripts/widen_ty_visibility.sh # sync only +scripts/widen_ty_visibility.sh --test # sync + run tests +``` + +The script: +1. Drops the visibility commit +2. Un-commits the script commit (files stay in working tree) +3. Hard-resets to `upstream/main` (no merge commits) +4. Re-commits the script files +5. Re-applies visibility widening via sed + fix-ups + cargo fmt +6. Runs `cargo check` (and `cargo test` with `--test`) + +## Adding new fix-ups + +If upstream introduces new items that break compilation after the blanket +`pub(crate)` → `pub` widening, add a targeted sed command in the "Fix-ups" +section of `widen_ty_visibility.sh`. diff --git a/scripts/widen_ty_visibility.sh b/scripts/widen_ty_visibility.sh new file mode 100755 index 0000000000000..4ec126f6a0b6b --- /dev/null +++ b/scripts/widen_ty_visibility.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Syncs this fork with upstream/main and re-applies the custom commits. +# +# This branch (ty-types-2) keeps two custom commits on top of upstream/main: +# 1. "Add widen_ty_visibility.sh script" — this script + CLAUDE.md +# 2. "Widen ty_python_semantic visibility to pub" — blanket pub widening +# +# The sync workflow: +# - Drops the visibility commit (if present) +# - Un-commits the script commit (keeping files in working tree) +# - Hard-resets to upstream/main (untracked files survive) +# - Re-commits the script files +# - Re-applies the visibility widening +# +# Usage: scripts/widen_ty_visibility.sh [--test] +# --test Also run tests after cargo check + +REPO_ROOT="$(git rev-parse --show-toplevel)" +TARGET="$REPO_ROOT/crates/ty_python_semantic" + +VIS_COMMIT_MSG="Widen ty_python_semantic visibility to pub" +SCRIPT_COMMIT_MSG="Add widen_ty_visibility.sh script" + +SCRIPT_FILES=( + "scripts/widen_ty_visibility.sh" + "scripts/CLAUDE.md" +) + +# Parse args +run_tests=false +for arg in "$@"; do + case "$arg" in + --test) run_tests=true ;; + --help|-h) + sed -n '4,18s/^# \?//p' "$0" + exit 0 + ;; + --*) echo "Unknown option: $arg"; exit 1 ;; + esac +done + +# ── Step 0: Validate ───────────────────────────────────────────────── +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Error: working tree is not clean." + exit 1 +fi + +# ── Step 1: Drop visibility commit ─────────────────────────────────── +last_msg="$(git log -1 --format=%s)" +if [[ "$last_msg" == "$VIS_COMMIT_MSG" ]]; then + echo "Step 1: Dropping visibility commit..." + git reset --hard HEAD~1 +else + echo "Step 1: No visibility commit to drop (HEAD: $last_msg)" +fi + +# ── Step 2: Un-commit script commit ────────────────────────────────── +last_msg="$(git log -1 --format=%s)" +if [[ "$last_msg" == "$SCRIPT_COMMIT_MSG" ]]; then + echo "Step 2: Un-committing script commit (files stay in working tree)..." + git reset --mixed HEAD~1 +else + echo "Step 2: No script commit to un-commit (HEAD: $last_msg)" +fi + +# ── Step 3: Reset to upstream/main ─────────────────────────────────── +echo "Step 3: Fetching upstream and resetting to upstream/main..." +git fetch upstream +git reset --hard upstream/main + +# ── Step 4: Re-commit script files ─────────────────────────────────── +echo "Step 4: Committing script files..." +cd "$REPO_ROOT" +git add "${SCRIPT_FILES[@]}" +git commit -m "$(cat <<'EOF' +Add widen_ty_visibility.sh script + +This branch (ty-types-2) is a fork of astral-sh/ruff that widens +visibility in ty_python_semantic from pub(crate)/pub(super) to pub +for consumption by OpenRewrite. + +To sync with upstream: scripts/widen_ty_visibility.sh +To sync and run tests: scripts/widen_ty_visibility.sh --test +EOF +)" + +# ── Step 5: Widen visibility and commit ────────────────────────────── +echo "Step 5: Widening visibility in ty_python_semantic..." + +# Blanket widen: convert all restricted visibility qualifiers to `pub` +find "$TARGET" -name '*.rs' -exec sed -i '' \ + -e 's/pub(super)/pub/g' \ + -e 's/pub(crate)/pub/g' \ + -e 's/pub(in [^)]*)/pub/g' \ + {} + + +# ── Fix-ups ────────────────────────────────────────────────────────── +# Items that can't simply become `pub`, or that the blanket sed misses. + +# `todo_type` is a macro_rules! macro — can't be `pub use` without #[macro_export] +sed -i '' 's/^pub use todo_type;$/pub(crate) use todo_type;/' \ + "$TARGET/src/types.rs" + +# `SynthesizedProtocolType` is re-exported from a private module +sed -i '' 's/^mod synthesized_protocol {$/pub mod synthesized_protocol {/' \ + "$TARGET/src/types/instance.rs" + +# Type::bindings() is a bare `fn` (no visibility qualifier) — not caught by +# the pub(crate)->pub sed. Make it public so external crates can call it. +sed -i '' '/^ fn bindings(self, db/s/^ fn / pub fn /' \ + "$TARGET/src/types.rs" + +# Private `mod` declarations in types.rs — the blanket sed only catches +# `pub(crate) mod`, not bare `mod`. Make them all public. +sed -i '' 's/^mod \([a-z_]*;\)/pub mod \1/' \ + "$TARGET/src/types.rs" + +# ── Format & commit ────────────────────────────────────────────────── +echo "Running cargo fmt -p ty_python_semantic..." +cargo fmt -p ty_python_semantic + +git add "$TARGET" +git commit -m "$VIS_COMMIT_MSG" +echo "Created visibility commit." + +# ── Step 6: Verify ─────────────────────────────────────────────────── +echo "Step 6: Running cargo check -p ty_python_semantic..." +cargo check -p ty_python_semantic + +if $run_tests; then + echo "Running cargo test -p ty_python_semantic..." + cargo test -p ty_python_semantic +fi + +echo "" +echo "Done. History:" +git log --oneline -3 From f74e9836f00d3ccebe993a551e7261a723a86dee Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Mon, 9 Mar 2026 10:09:44 +0100 Subject: [PATCH 261/261] Widen ty_python_semantic visibility to pub --- crates/ty_python_semantic/src/ast_node_ref.rs | 4 +- crates/ty_python_semantic/src/db.rs | 22 +- .../src/diagnostic/levenshtein.rs | 4 +- .../ty_python_semantic/src/diagnostic/mod.rs | 8 +- crates/ty_python_semantic/src/dunder_all.rs | 2 +- crates/ty_python_semantic/src/lib.rs | 2 +- crates/ty_python_semantic/src/lint.rs | 2 +- crates/ty_python_semantic/src/node_key.rs | 6 +- crates/ty_python_semantic/src/place.rs | 187 ++++---- crates/ty_python_semantic/src/rank.rs | 8 +- .../ty_python_semantic/src/semantic_index.rs | 129 +++-- .../src/semantic_index/ast_ids.rs | 12 +- .../src/semantic_index/builder.rs | 6 +- .../semantic_index/builder/except_handlers.rs | 12 +- .../builder/loop_bindings_visitor.rs | 8 +- .../src/semantic_index/definition.rs | 250 +++++----- .../src/semantic_index/expression.rs | 22 +- .../src/semantic_index/member.rs | 73 ++- .../semantic_index/narrowing_constraints.rs | 4 +- .../src/semantic_index/place.rs | 91 ++-- .../src/semantic_index/predicate.rs | 66 +-- .../src/semantic_index/re_exports.rs | 2 +- .../reachability_constraints.rs | 31 +- .../src/semantic_index/scope.rs | 92 ++-- .../src/semantic_index/symbol.rs | 50 +- .../src/semantic_index/use_def.rs | 193 ++++---- .../src/semantic_index/use_def/place_state.rs | 82 ++-- .../ty_python_semantic/src/semantic_model.rs | 2 +- crates/ty_python_semantic/src/subscript.rs | 12 +- crates/ty_python_semantic/src/suppression.rs | 29 +- .../src/suppression/parser.rs | 20 +- .../src/suppression/unused.rs | 2 +- crates/ty_python_semantic/src/types.rs | 442 +++++++++--------- crates/ty_python_semantic/src/types/bool.rs | 14 +- .../src/types/bound_super.rs | 18 +- crates/ty_python_semantic/src/types/call.rs | 26 +- .../src/types/call/arguments.rs | 34 +- .../ty_python_semantic/src/types/call/bind.rs | 150 +++--- .../ty_python_semantic/src/types/callable.rs | 72 ++- crates/ty_python_semantic/src/types/class.rs | 244 +++++----- .../src/types/class/dynamic_literal.rs | 42 +- .../src/types/class/known.rs | 50 +- .../src/types/class/named_tuple.rs | 44 +- .../src/types/class/static_literal.rs | 129 +++-- .../src/types/class_base.rs | 26 +- .../src/types/constraints.rs | 78 ++-- .../ty_python_semantic/src/types/context.rs | 50 +- .../src/types/context_manager.rs | 12 +- crates/ty_python_semantic/src/types/cyclic.rs | 8 +- .../src/types/definition.rs | 2 +- .../src/types/diagnostic.rs | 359 +++++++------- .../ty_python_semantic/src/types/display.rs | 62 ++- crates/ty_python_semantic/src/types/enums.rs | 42 +- .../ty_python_semantic/src/types/function.rs | 149 +++--- .../ty_python_semantic/src/types/generics.rs | 140 +++--- .../src/types/ide_support.rs | 6 +- crates/ty_python_semantic/src/types/infer.rs | 91 ++-- .../src/types/infer/builder.rs | 16 +- .../infer/builder/annotation_expression.rs | 6 +- .../types/infer/builder/binary_expressions.rs | 6 +- .../src/types/infer/builder/class.rs | 14 +- .../src/types/infer/builder/function.rs | 16 +- .../src/types/infer/builder/imports.rs | 10 +- .../src/types/infer/builder/named_tuple.rs | 8 +- .../infer/builder/paramspec_validation.rs | 2 +- .../src/types/infer/builder/subscript.rs | 23 +- .../types/infer/builder/type_expression.rs | 25 +- .../src/types/infer/builder/typevar.rs | 18 +- .../src/types/infer/comparisons.rs | 12 +- .../src/types/infer/deferred/dynamic_class.rs | 2 +- .../types/infer/deferred/final_variable.rs | 5 +- .../src/types/infer/deferred/function.rs | 2 +- .../src/types/infer/deferred/mod.rs | 14 +- .../infer/deferred/overloaded_function.rs | 2 +- .../src/types/infer/deferred/static_class.rs | 2 +- .../infer/deferred/type_param_validation.rs | 2 +- .../src/types/infer/deferred/typeguard.rs | 2 +- .../ty_python_semantic/src/types/instance.rs | 106 ++--- .../ty_python_semantic/src/types/iteration.rs | 12 +- .../src/types/known_instance.rs | 28 +- .../src/types/list_members.rs | 4 +- .../ty_python_semantic/src/types/literal.rs | 58 ++- crates/ty_python_semantic/src/types/member.rs | 18 +- crates/ty_python_semantic/src/types/method.rs | 32 +- crates/ty_python_semantic/src/types/mro.rs | 48 +- crates/ty_python_semantic/src/types/narrow.rs | 20 +- .../ty_python_semantic/src/types/newtype.rs | 12 +- .../ty_python_semantic/src/types/overrides.rs | 4 +- .../src/types/property_tests/setup.rs | 2 +- .../types/property_tests/type_generation.rs | 21 +- .../src/types/protocol_class.rs | 58 +-- .../ty_python_semantic/src/types/relation.rs | 50 +- .../src/types/set_theoretic.rs | 79 ++-- .../src/types/set_theoretic/builder.rs | 46 +- .../src/types/signatures.rs | 185 ++++---- .../src/types/special_form.rs | 36 +- .../src/types/string_annotation.rs | 14 +- .../src/types/subclass_of.rs | 58 +-- .../ty_python_semantic/src/types/subscript.rs | 22 +- crates/ty_python_semantic/src/types/tuple.rs | 149 +++--- .../src/types/type_alias.rs | 44 +- .../src/types/typed_dict.rs | 74 +-- .../ty_python_semantic/src/types/typevar.rs | 126 +++-- .../ty_python_semantic/src/types/unpacker.rs | 27 +- .../ty_python_semantic/src/types/variance.rs | 16 +- .../ty_python_semantic/src/types/visitor.rs | 18 +- crates/ty_python_semantic/src/unpack.rs | 36 +- 107 files changed, 2525 insertions(+), 2788 deletions(-) diff --git a/crates/ty_python_semantic/src/ast_node_ref.rs b/crates/ty_python_semantic/src/ast_node_ref.rs index a3d1fae49abc8..33aae6f30d2b6 100644 --- a/crates/ty_python_semantic/src/ast_node_ref.rs +++ b/crates/ty_python_semantic/src/ast_node_ref.rs @@ -52,7 +52,7 @@ pub struct AstNodeRef { } impl AstNodeRef { - pub(crate) fn index(&self) -> NodeIndex { + pub fn index(&self) -> NodeIndex { self.index } } @@ -67,7 +67,7 @@ where /// /// This method may panic or produce unspecified results if the provided module is from a /// different file or Salsa revision than the module to which the node belongs. - pub(super) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self { + pub fn new(module_ref: &ParsedModuleRef, node: &T) -> Self { let index = node.node_index().load(); debug_assert_eq!(module_ref.get_by_index(index).try_into().ok(), Some(node)); diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index f6a1a2f17c2b2..b69ea1d690895 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -21,7 +21,7 @@ pub trait Db: ModuleResolverDb { } #[cfg(test)] -pub(crate) mod tests { +pub mod tests { use std::sync::{Arc, Mutex}; use crate::program::Program; @@ -48,7 +48,7 @@ pub(crate) mod tests { #[salsa::db] #[derive(Clone)] - pub(crate) struct TestDb { + pub struct TestDb { storage: salsa::Storage, files: Files, system: TestSystem, @@ -59,7 +59,7 @@ pub(crate) mod tests { } impl TestDb { - pub(crate) fn new() -> Self { + pub fn new() -> Self { let events = Events::default(); Self { storage: salsa::Storage::new(Some(Box::new({ @@ -80,7 +80,7 @@ pub(crate) mod tests { } /// Takes the salsa events. - pub(crate) fn take_salsa_events(&mut self) -> Vec { + pub fn take_salsa_events(&mut self) -> Vec { let mut events = self.events.lock().unwrap(); std::mem::take(&mut *events) @@ -90,7 +90,7 @@ pub(crate) mod tests { /// /// ## Panics /// If there are any pending salsa snapshots. - pub(crate) fn clear_salsa_events(&mut self) { + pub fn clear_salsa_events(&mut self) { self.take_salsa_events(); } } @@ -157,7 +157,7 @@ pub(crate) mod tests { #[salsa::db] impl salsa::Database for TestDb {} - pub(crate) struct TestDbBuilder<'a> { + pub struct TestDbBuilder<'a> { /// Target Python version python_version: PythonVersion, /// Target Python platform @@ -167,7 +167,7 @@ pub(crate) mod tests { } impl<'a> TestDbBuilder<'a> { - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self { python_version: PythonVersion::default(), python_platform: PythonPlatform::default(), @@ -175,12 +175,12 @@ pub(crate) mod tests { } } - pub(crate) fn with_python_version(mut self, version: PythonVersion) -> Self { + pub fn with_python_version(mut self, version: PythonVersion) -> Self { self.python_version = version; self } - pub(crate) fn with_file( + pub fn with_file( mut self, path: &'a (impl AsRef + ?Sized), content: &'a str, @@ -189,7 +189,7 @@ pub(crate) mod tests { self } - pub(crate) fn build(self) -> anyhow::Result { + pub fn build(self) -> anyhow::Result { let mut db = TestDb::new(); let src_root = SystemPathBuf::from("/src"); @@ -216,7 +216,7 @@ pub(crate) mod tests { } } - pub(crate) fn setup_db() -> TestDb { + pub fn setup_db() -> TestDb { TestDbBuilder::new().build().expect("valid TestDb setup") } } diff --git a/crates/ty_python_semantic/src/diagnostic/levenshtein.rs b/crates/ty_python_semantic/src/diagnostic/levenshtein.rs index d1f3f0c54e98d..e2f62d74ce06e 100644 --- a/crates/ty_python_semantic/src/diagnostic/levenshtein.rs +++ b/crates/ty_python_semantic/src/diagnostic/levenshtein.rs @@ -12,7 +12,7 @@ use std::collections::BTreeSet; /// /// If the typo itself starts with an underscore, this policy is ignored. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum HideUnderscoredSuggestions { +pub enum HideUnderscoredSuggestions { Yes, #[cfg_attr(not(test), expect(dead_code))] No, @@ -24,7 +24,7 @@ impl HideUnderscoredSuggestions { } } -pub(super) fn find_best_suggestion<'a, O, I>( +pub fn find_best_suggestion<'a, O, I>( options: O, typo: &str, hide_underscored_suggestions: HideUnderscoredSuggestions, diff --git a/crates/ty_python_semantic/src/diagnostic/mod.rs b/crates/ty_python_semantic/src/diagnostic/mod.rs index 8b884363f3991..27bb09207e7fc 100644 --- a/crates/ty_python_semantic/src/diagnostic/mod.rs +++ b/crates/ty_python_semantic/src/diagnostic/mod.rs @@ -12,7 +12,7 @@ use std::fmt::Write; mod levenshtein; /// Suggest a name from `existing_names` that is similar to `wrong_name`. -pub(crate) fn did_you_mean<'a, O, I>(options: O, typo: &str) -> Option<&'a str> +pub fn did_you_mean<'a, O, I>(options: O, typo: &str) -> Option<&'a str> where O: IntoIterator, I: ExactSizeIterator, @@ -113,7 +113,7 @@ pub fn add_inferred_python_version_hint_to_diagnostic( /// Format a list of elements as a human-readable enumeration. /// /// Encloses every element in backticks (`1`, `2` and `3`). -pub(crate) fn format_enumeration(elements: I) -> String +pub fn format_enumeration(elements: I) -> String where I: IntoIterator, IT: ExactSizeIterator + DoubleEndedIterator, @@ -146,7 +146,7 @@ where /// associated file is equivalent to the file being type checked. As a result, /// if either is violated, then the `Drop` impl on `DiagnosticGuard` will /// panic. -pub(super) struct DiagnosticGuard<'sink> { +pub struct DiagnosticGuard<'sink> { /// The file of the primary span (to which file does this diagnostic belong). file: File, @@ -173,7 +173,7 @@ pub(super) struct DiagnosticGuard<'sink> { } impl<'sink> DiagnosticGuard<'sink> { - pub(crate) fn new( + pub fn new( file: File, sink: &'sink std::cell::RefCell, diag: Diagnostic, diff --git a/crates/ty_python_semantic/src/dunder_all.rs b/crates/ty_python_semantic/src/dunder_all.rs index 11870a77d859f..7f57e70c23a9a 100644 --- a/crates/ty_python_semantic/src/dunder_all.rs +++ b/crates/ty_python_semantic/src/dunder_all.rs @@ -14,7 +14,7 @@ use crate::types::{Truthiness, Type, TypeContext, infer_expression_types}; /// Returns a set of names in the `__all__` variable for `file`, [`None`] if it is not defined or /// if it contains invalid elements. #[salsa::tracked(returns(as_ref), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option> { +pub fn dunder_all_names(db: &dyn Db, file: File) -> Option> { let _span = tracing::trace_span!("dunder_all_names", file=?file.path(db)).entered(); let module = parsed_module(db, file).load(db); diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 3d016805c6387..ec2c3ab192848 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -39,7 +39,7 @@ mod db; mod dunder_all; pub mod lint; mod node_key; -pub(crate) mod place; +pub mod place; mod program; mod python_platform; mod rank; diff --git a/crates/ty_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs index 5606f03397a13..7d949fcedcab1 100644 --- a/crates/ty_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -234,7 +234,7 @@ impl LintStatus { /// /// ```python /// /// print(x) # NameError: name 'x' is not defined /// /// ``` -/// pub(crate) static UNRESOLVED_REFERENCE = { +/// pub static UNRESOLVED_REFERENCE = { /// summary: "detects references to names that are not defined", /// status: LintStatus::preview("1.0.0"), /// default_level: Level::Warn, diff --git a/crates/ty_python_semantic/src/node_key.rs b/crates/ty_python_semantic/src/node_key.rs index a93931294b228..917d53f9e73bc 100644 --- a/crates/ty_python_semantic/src/node_key.rs +++ b/crates/ty_python_semantic/src/node_key.rs @@ -4,17 +4,17 @@ use crate::ast_node_ref::AstNodeRef; /// Compact key for a node for use in a hash map. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] -pub(super) struct NodeKey(NodeIndex); +pub struct NodeKey(NodeIndex); impl NodeKey { - pub(super) fn from_node(node: N) -> Self + pub fn from_node(node: N) -> Self where N: HasNodeIndex, { NodeKey(node.node_index().load()) } - pub(super) fn from_node_ref(node_ref: &AstNodeRef) -> Self { + pub fn from_node_ref(node_ref: &AstNodeRef) -> Self { NodeKey(node_ref.index()) } } diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index bd9618f93ef30..c2ec2f6fa4866 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -20,18 +20,18 @@ use crate::types::{ }; use crate::{Db, FxIndexSet, FxOrderSet, Program}; -pub(crate) use implicit_globals::{ +pub use implicit_globals::{ module_type_implicit_global_declaration, module_type_implicit_global_symbol, }; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize)] -pub(crate) enum Definedness { +pub enum Definedness { AlwaysDefined, PossiblyUndefined, } impl Definedness { - pub(crate) const fn max(self, other: Self) -> Self { + pub const fn max(self, other: Self) -> Self { match (self, other) { (Definedness::AlwaysDefined, _) | (_, Definedness::AlwaysDefined) => { Definedness::AlwaysDefined @@ -44,17 +44,17 @@ impl Definedness { } #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize)] -pub(crate) enum TypeOrigin { +pub enum TypeOrigin { Declared, Inferred, } impl TypeOrigin { - pub(crate) const fn is_declared(self) -> bool { + pub const fn is_declared(self) -> bool { matches!(self, TypeOrigin::Declared) } - pub(crate) const fn merge(self, other: Self) -> Self { + pub const fn merge(self, other: Self) -> Self { match (self, other) { (TypeOrigin::Declared, TypeOrigin::Declared) => TypeOrigin::Declared, _ => TypeOrigin::Inferred, @@ -71,7 +71,7 @@ impl TypeOrigin { /// This enum tracks whether such widening should be applied, allowing callers /// to access either the raw inferred type or the widened public type. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, get_size2::GetSize)] -pub(crate) enum Widening { +pub enum Widening { /// The type should not be widened with `Unknown`. #[default] None, @@ -81,7 +81,7 @@ pub(crate) enum Widening { impl Widening { /// Apply widening to the type if this is `WithUnknown`. - pub(crate) fn apply_if_needed<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + pub fn apply_if_needed<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { Self::None => ty, Self::WithUnknown => UnionType::from_two_elements(db, Type::unknown(), ty), @@ -91,15 +91,15 @@ impl Widening { /// A defined place with its type, origin, definedness, and widening information. #[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct DefinedPlace<'db> { - pub(crate) ty: Type<'db>, - pub(crate) origin: TypeOrigin, - pub(crate) definedness: Definedness, - pub(crate) widening: Widening, +pub struct DefinedPlace<'db> { + pub ty: Type<'db>, + pub origin: TypeOrigin, + pub definedness: Definedness, + pub widening: Widening, } impl<'db> DefinedPlace<'db> { - pub(crate) fn new(ty: Type<'db>) -> Self { + pub fn new(ty: Type<'db>) -> Self { Self { ty, origin: TypeOrigin::Inferred, @@ -108,22 +108,22 @@ impl<'db> DefinedPlace<'db> { } } - pub(crate) fn with_origin(mut self, origin: TypeOrigin) -> Self { + pub fn with_origin(mut self, origin: TypeOrigin) -> Self { self.origin = origin; self } - pub(crate) fn with_definedness(mut self, definedness: Definedness) -> Self { + pub fn with_definedness(mut self, definedness: Definedness) -> Self { self.definedness = definedness; self } - pub(crate) fn with_widening(mut self, widening: Widening) -> Self { + pub fn with_widening(mut self, widening: Widening) -> Self { self.widening = widening; self } - pub(crate) const fn is_definitely_defined(&self) -> bool { + pub const fn is_definitely_defined(&self) -> bool { matches!(self.definedness, Definedness::AlwaysDefined) } } @@ -158,7 +158,7 @@ impl<'db> DefinedPlace<'db> { /// non_existent: Place::Undefined, /// ``` #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) enum Place<'db> { +pub enum Place<'db> { Defined(DefinedPlace<'db>), #[default] Undefined, @@ -166,16 +166,16 @@ pub(crate) enum Place<'db> { impl<'db> Place<'db> { /// Constructor that creates a [`Place`] with type origin [`TypeOrigin::Inferred`] and definedness [`Definedness::AlwaysDefined`]. - pub(crate) fn bound(ty: impl Into>) -> Self { + pub fn bound(ty: impl Into>) -> Self { Place::Defined(DefinedPlace::new(ty.into())) } /// Constructor that creates a [`Place`] with type origin [`TypeOrigin::Declared`] and definedness [`Definedness::AlwaysDefined`]. - pub(crate) fn declared(ty: impl Into>) -> Self { + pub fn declared(ty: impl Into>) -> Self { Place::Defined(DefinedPlace::new(ty.into()).with_origin(TypeOrigin::Declared)) } - pub(crate) fn is_undefined(&self) -> bool { + pub fn is_undefined(&self) -> bool { matches!(self, Place::Undefined) } @@ -183,7 +183,7 @@ impl<'db> Place<'db> { /// /// If the place is *definitely* undefined, this function will return `None`. Otherwise, /// if there is at least one control-flow path where the place is defined, return the type. - pub(crate) fn ignore_possibly_undefined(&self) -> Option> { + pub fn ignore_possibly_undefined(&self) -> Option> { match self { Place::Defined(defined) => Some(defined.ty), Place::Undefined => None, @@ -194,7 +194,7 @@ impl<'db> Place<'db> { /// /// The stored type is always the unwidened type. Widening (union with `Unknown`) /// is applied lazily when converting to `LookupResult`. - pub(crate) fn unwidened_type(&self) -> Option> { + pub fn unwidened_type(&self) -> Option> { match self { Place::Defined(defined) => Some(defined.ty), Place::Undefined => None, @@ -203,13 +203,13 @@ impl<'db> Place<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_type(self) -> Type<'db> { + pub fn expect_type(self) -> Type<'db> { self.ignore_possibly_undefined() .expect("Expected a (possibly undefined) type, not an undefined place") } #[must_use] - pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Place<'db> { + pub fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Place<'db> { match self { Place::Defined(defined) => Place::Defined(DefinedPlace { ty: f(defined.ty), @@ -221,7 +221,7 @@ impl<'db> Place<'db> { /// Set the widening mode for this place. #[must_use] - pub(crate) fn with_widening(self, new_widening: Widening) -> Place<'db> { + pub fn with_widening(self, new_widening: Widening) -> Place<'db> { match self { Place::Defined(defined) => Place::Defined(defined.with_widening(new_widening)), Place::Undefined => Place::Undefined, @@ -229,7 +229,7 @@ impl<'db> Place<'db> { } #[must_use] - pub(crate) fn with_qualifiers(self, qualifiers: TypeQualifiers) -> PlaceAndQualifiers<'db> { + pub fn with_qualifiers(self, qualifiers: TypeQualifiers) -> PlaceAndQualifiers<'db> { PlaceAndQualifiers { place: self, qualifiers, @@ -239,7 +239,7 @@ impl<'db> Place<'db> { /// Try to call `__get__(None, owner)` on the type of this place (not on the meta type). /// If it succeeds, return the `__get__` return type. Otherwise, returns the original place. /// This is used to resolve (potential) descriptor attributes. - pub(crate) fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Place<'db> { + pub fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Place<'db> { match self { Place::Defined( place @ DefinedPlace { @@ -276,7 +276,7 @@ impl<'db> Place<'db> { } } - pub(crate) const fn is_definitely_bound(&self) -> bool { + pub const fn is_definitely_bound(&self) -> bool { matches!( self, Place::Defined(DefinedPlace { @@ -308,14 +308,14 @@ impl<'db> From> for PlaceAndQualifiers<'db> { /// Possible ways in which a place lookup can (possibly or definitely) fail. #[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub(crate) enum LookupError<'db> { +pub enum LookupError<'db> { Undefined(TypeQualifiers), PossiblyUndefined(TypeAndQualifiers<'db>), } impl<'db> LookupError<'db> { /// Fallback (wholly or partially) to `fallback` to create a new [`LookupResult`]. - pub(crate) fn or_fall_back_to( + pub fn or_fall_back_to( self, db: &'db dyn Db, fallback: PlaceAndQualifiers<'db>, @@ -345,12 +345,12 @@ impl<'db> LookupError<'db> { /// /// Note that this type is exactly isomorphic to [`Place`]. /// In the future, we could possibly consider removing `Place` and using this type everywhere instead. -pub(crate) type LookupResult<'db> = Result, LookupError<'db>>; +pub type LookupResult<'db> = Result, LookupError<'db>>; /// Infer the public type of a symbol (its type as seen from outside its scope) in the given /// `scope`. #[allow(unused)] -pub(crate) fn symbol<'db>( +pub fn symbol<'db>( db: &'db dyn Db, scope: ScopeId<'db>, name: &str, @@ -367,7 +367,7 @@ pub(crate) fn symbol<'db>( /// Infer the public type of a place (its type as seen from outside its scope) in the given /// `scope`. -pub(crate) fn place<'db>( +pub fn place<'db>( db: &'db dyn Db, scope: ScopeId<'db>, member: PlaceExprRef, @@ -390,7 +390,7 @@ pub(crate) fn place<'db>( /// those additional symbols. /// /// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). -pub(crate) fn explicit_global_symbol<'db>( +pub fn explicit_global_symbol<'db>( db: &'db dyn Db, file: File, name: &str, @@ -412,11 +412,7 @@ pub(crate) fn explicit_global_symbol<'db>( /// /// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). #[allow(unused)] -pub(crate) fn global_symbol<'db>( - db: &'db dyn Db, - file: File, - name: &str, -) -> PlaceAndQualifiers<'db> { +pub fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> PlaceAndQualifiers<'db> { explicit_global_symbol(db, file, name) .or_fall_back_to(db, || module_type_implicit_global_symbol(db, name)) } @@ -427,7 +423,7 @@ pub(crate) fn global_symbol<'db>( /// For stub files, explicit re-export will be required, while for non-stub files, it will not. /// /// `None` should be passed for the `file` parameter if looking up a symbol on a namespace package. -pub(crate) fn imported_symbol<'db>( +pub fn imported_symbol<'db>( db: &'db dyn Db, file: Option, name: &str, @@ -502,7 +498,7 @@ pub(crate) fn imported_symbol<'db>( /// Note that this function is only intended for use in the context of the builtins *namespace* /// and should not be used when a symbol is being explicitly imported from the `builtins` module /// (e.g. `from builtins import int`). -pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { +pub fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { let resolver = |module: Module<'_>| { let file = module.file(db)?; let found_symbol = symbol_impl( @@ -532,7 +528,7 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQua /// Lookup the type of `symbol` in a given known module. /// /// Returns `Place::Undefined` if the given known module cannot be resolved for some reason. -pub(crate) fn known_module_symbol<'db>( +pub fn known_module_symbol<'db>( db: &'db dyn Db, known_module: KnownModule, symbol: &str, @@ -550,7 +546,7 @@ pub(crate) fn known_module_symbol<'db>( /// Returns `Place::Undefined` if the `typing` module isn't available for some reason. #[inline] #[cfg(test)] -pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { +pub fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { known_module_symbol(db, KnownModule::Typing, symbol) } @@ -558,17 +554,14 @@ pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQuali /// /// Returns `Place::Undefined` if the `typing_extensions` module isn't available for some reason. #[inline] -pub(crate) fn typing_extensions_symbol<'db>( - db: &'db dyn Db, - symbol: &str, -) -> PlaceAndQualifiers<'db> { +pub fn typing_extensions_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { known_module_symbol(db, KnownModule::TypingExtensions, symbol) } /// Get the `builtins` module scope. /// /// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`. -pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option> { +pub fn builtins_module_scope(db: &dyn Db) -> Option> { core_module_scope(db, KnownModule::Builtins) } @@ -584,7 +577,7 @@ fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option( +pub fn place_from_bindings<'db>( db: &'db dyn Db, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, ) -> PlaceWithDefinition<'db> { @@ -599,7 +592,7 @@ pub(super) fn place_from_bindings<'db>( /// /// This function also returns declaredness information (see [`Place`]) and a set of /// [`TypeQualifiers`] that have been specified on the declaration(s). -pub(crate) fn place_from_declarations<'db>( +pub fn place_from_declarations<'db>( db: &'db dyn Db, declarations: DeclarationsIterator<'_, 'db>, ) -> PlaceFromDeclarationsResult<'db> { @@ -613,12 +606,12 @@ type DeclaredTypeAndConflictingTypes<'db> = ( /// The result of looking up a declared type from declarations; see [`place_from_declarations`]. #[derive(Debug, Default)] -pub(crate) struct PlaceFromDeclarationsResult<'db> { +pub struct PlaceFromDeclarationsResult<'db> { place_and_quals: PlaceAndQualifiers<'db>, conflicting_types: Option>>>, /// Contains the first reachable declaration for this place, if any. /// This field is used for backreferences in diagnostics. - pub(crate) first_declaration: Option>, + pub first_declaration: Option>, } impl<'db> PlaceFromDeclarationsResult<'db> { @@ -634,11 +627,11 @@ impl<'db> PlaceFromDeclarationsResult<'db> { } } - pub(crate) fn ignore_conflicting_declarations(self) -> PlaceAndQualifiers<'db> { + pub fn ignore_conflicting_declarations(self) -> PlaceAndQualifiers<'db> { self.place_and_quals } - pub(crate) fn into_place_and_conflicting_declarations( + pub fn into_place_and_conflicting_declarations( self, ) -> ( PlaceAndQualifiers<'db>, @@ -663,51 +656,51 @@ impl<'db> PlaceFromDeclarationsResult<'db> { /// /// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR #[derive(Debug, Clone, Default, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct PlaceAndQualifiers<'db> { - pub(crate) place: Place<'db>, - pub(crate) qualifiers: TypeQualifiers, +pub struct PlaceAndQualifiers<'db> { + pub place: Place<'db>, + pub qualifiers: TypeQualifiers, } impl<'db> PlaceAndQualifiers<'db> { - pub(crate) fn unbound() -> Self { + pub fn unbound() -> Self { Self::default() } - pub(crate) fn is_undefined(&self) -> bool { + pub fn is_undefined(&self) -> bool { self.place.is_undefined() } - pub(crate) fn ignore_possibly_undefined(&self) -> Option> { + pub fn ignore_possibly_undefined(&self) -> Option> { self.place.ignore_possibly_undefined() } /// Returns `true` if the place has a `ClassVar` type qualifier. - pub(crate) fn is_class_var(&self) -> bool { + pub fn is_class_var(&self) -> bool { self.qualifiers.contains(TypeQualifiers::CLASS_VAR) } /// Returns `true` if the place has a `InitVar` type qualifier. - pub(crate) fn is_init_var(&self) -> bool { + pub fn is_init_var(&self) -> bool { self.qualifiers.contains(TypeQualifiers::INIT_VAR) } /// Returns `true` if the place has a `Required` type qualifier. - pub(crate) fn is_required(&self) -> bool { + pub fn is_required(&self) -> bool { self.qualifiers.contains(TypeQualifiers::REQUIRED) } /// Returns `true` if the place has a `NotRequired` type qualifier. - pub(crate) fn is_not_required(&self) -> bool { + pub fn is_not_required(&self) -> bool { self.qualifiers.contains(TypeQualifiers::NOT_REQUIRED) } /// Returns `true` if the place has a `ReadOnly` type qualifier. - pub(crate) fn is_read_only(&self) -> bool { + pub fn is_read_only(&self) -> bool { self.qualifiers.contains(TypeQualifiers::READ_ONLY) } /// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type. - pub(crate) fn is_bare_final(&self) -> Option { + pub fn is_bare_final(&self) -> Option { match self { PlaceAndQualifiers { place, qualifiers } if (qualifiers.contains(TypeQualifiers::FINAL) @@ -722,10 +715,7 @@ impl<'db> PlaceAndQualifiers<'db> { } #[must_use] - pub(crate) fn map_type( - self, - f: impl FnOnce(Type<'db>) -> Type<'db>, - ) -> PlaceAndQualifiers<'db> { + pub fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> PlaceAndQualifiers<'db> { PlaceAndQualifiers { place: self.place.map_type(f), qualifiers: self.qualifiers, @@ -738,7 +728,7 @@ impl<'db> PlaceAndQualifiers<'db> { /// /// For places marked with `Widening::WithUnknown`, this applies the gradual typing guarantee /// by creating a union with `Unknown`. - pub(crate) fn into_lookup_result(self, db: &'db dyn Db) -> LookupResult<'db> { + pub fn into_lookup_result(self, db: &'db dyn Db) -> LookupResult<'db> { match self { PlaceAndQualifiers { place: Place::Defined(place), @@ -766,7 +756,7 @@ impl<'db> PlaceAndQualifiers<'db> { /// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning /// the result of `diagnostic_fn` (which will be a [`TypeAndQualifiers`]). This allows the caller /// to ensure that a diagnostic is emitted if the place is possibly or definitely unbound. - pub(crate) fn unwrap_with_diagnostic( + pub fn unwrap_with_diagnostic( self, db: &'db dyn Db, diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>, @@ -785,7 +775,7 @@ impl<'db> PlaceAndQualifiers<'db> { /// 4. Else, if `self` is possibly unbound and `fallback` is possibly unbound, /// return `Place(, Definedness::PossiblyUndefined)` #[must_use] - pub(crate) fn or_fall_back_to( + pub fn or_fall_back_to( self, db: &'db dyn Db, fallback_fn: impl FnOnce() -> PlaceAndQualifiers<'db>, @@ -795,7 +785,7 @@ impl<'db> PlaceAndQualifiers<'db> { .into() } - pub(crate) fn cycle_normalized( + pub fn cycle_normalized( self, db: &'db dyn Db, previous_place: Self, @@ -855,7 +845,7 @@ impl<'db> From> for PlaceAndQualifiers<'db> { }, heap_size=ruff_memory_usage::heap_size )] -pub(crate) fn place_by_id<'db>( +pub fn place_by_id<'db>( db: &'db dyn Db, scope: ScopeId<'db>, place_id: ScopedPlaceId, @@ -1150,7 +1140,7 @@ fn place_impl<'db>( cycle_fn=loop_header_reachability_cycle_recover, heap_size = ruff_memory_usage::heap_size, )] -pub(crate) fn loop_header_reachability<'db>( +pub fn loop_header_reachability<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> LoopHeaderReachability<'db> { @@ -1229,12 +1219,12 @@ fn loop_header_reachability_impl<'db>( /// Result of [`loop_header_reachability`]: pre-computed reachability info for loop-back bindings. #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct LoopHeaderReachability<'db> { +pub struct LoopHeaderReachability<'db> { /// Whether any reachable loop-back binding is a defined binding. - pub(crate) has_defined_bindings: bool, - pub(crate) deleted_reachability: Truthiness, + pub has_defined_bindings: bool, + pub deleted_reachability: Truthiness, /// Reachable loop-back bindings that are not `del`s. - pub(crate) reachable_bindings: FxIndexSet>, + pub reachable_bindings: FxIndexSet>, } impl<'db> LoopHeaderReachability<'db> { @@ -1256,9 +1246,9 @@ impl<'db> LoopHeaderReachability<'db> { /// A single reachable loop-back binding with its narrowing constraint. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) struct ReachableLoopBinding<'db> { - pub(crate) definition: Definition<'db>, - pub(crate) narrowing_constraint: ScopedNarrowingConstraint, +pub struct ReachableLoopBinding<'db> { + pub definition: Definition<'db>, + pub narrowing_constraint: ScopedNarrowingConstraint, } /// Implementation of [`place_from_bindings`]. @@ -1458,9 +1448,9 @@ fn place_from_bindings_impl<'db>( } } -pub(super) struct PlaceWithDefinition<'db> { - pub(super) place: Place<'db>, - pub(super) first_definition: Option>, +pub struct PlaceWithDefinition<'db> { + pub place: Place<'db>, + pub first_definition: Option>, } /// Accumulates types from multiple bindings or declarations, and eventually builds a @@ -1718,7 +1708,7 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { all_names.contains(symbol_name) } -pub(crate) mod implicit_globals { +pub mod implicit_globals { use ruff_python_ast as ast; use ruff_python_ast::name::Name; @@ -1734,7 +1724,7 @@ pub(crate) mod implicit_globals { use super::{DefinedPlace, Place, place_from_declarations}; - pub(crate) fn module_type_implicit_global_declaration<'db>( + pub fn module_type_implicit_global_declaration<'db>( db: &'db dyn Db, name: &str, ) -> PlaceAndQualifiers<'db> { @@ -1777,7 +1767,7 @@ pub(crate) mod implicit_globals { /// [`Place::Undefined`] for `__init__` and `__dict__` (which cannot be found in globals if /// the lookup is being done from the same file) -- but these symbols *are* available in the /// global scope if they're being imported **from a different file**. - pub(crate) fn module_type_implicit_global_symbol<'db>( + pub fn module_type_implicit_global_symbol<'db>( db: &'db dyn Db, name: &str, ) -> PlaceAndQualifiers<'db> { @@ -1903,9 +1893,7 @@ pub(crate) mod implicit_globals { /// This is used for completions in the global scope of a module. It returns /// the correct types for special-cased symbols like `__file__` (which is `str` /// for the current module, not `str | None`). - pub(crate) fn all_implicit_module_globals( - db: &dyn Db, - ) -> impl Iterator)> + '_ { + pub fn all_implicit_module_globals(db: &dyn Db) -> impl Iterator)> + '_ { // Special-cased implicit globals that are not in `module_type_symbols` let special_cased = ["__builtins__", "__debug__", "__warningregistry__"] .into_iter() @@ -1951,10 +1939,7 @@ pub(crate) mod implicit_globals { /// class creation. /// /// See -pub(crate) fn class_body_implicit_symbol<'db>( - db: &'db dyn Db, - name: &str, -) -> PlaceAndQualifiers<'db> { +pub fn class_body_implicit_symbol<'db>(db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { match name { "__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(), "__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(), @@ -1974,7 +1959,7 @@ pub(crate) fn class_body_implicit_symbol<'db>( } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub(crate) enum RequiresExplicitReExport { +pub enum RequiresExplicitReExport { Yes, No, } @@ -1999,7 +1984,7 @@ impl RequiresExplicitReExport { /// x = 3 /// ``` #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub(crate) enum ConsideredDefinitions { +pub enum ConsideredDefinitions { /// Consider only the definitions that are "live" at the end of the scope, i.e. those /// that have not been shadowed or deleted. EndOfScope, @@ -2009,7 +1994,7 @@ pub(crate) enum ConsideredDefinitions { /// Specifies how the boundness of a place should be determined. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub(crate) enum BoundnessAnalysis { +pub enum BoundnessAnalysis { /// The place is always considered bound. AssumeBound, /// The boundness of the place is determined based on the visibility of the implicit diff --git a/crates/ty_python_semantic/src/rank.rs b/crates/ty_python_semantic/src/rank.rs index 2d06d6f638fd4..99262d093ac0f 100644 --- a/crates/ty_python_semantic/src/rank.rs +++ b/crates/ty_python_semantic/src/rank.rs @@ -24,7 +24,7 @@ use get_size2::GetSize; /// This trick adds O(1.5) bits of overhead per large vector element on 64-bit platforms, and O(2) /// bits of overhead on 32-bit platforms. #[derive(Clone, Debug, Eq, PartialEq, GetSize)] -pub(crate) struct RankBitBox { +pub struct RankBitBox { #[get_size(size_fn = bit_box_size)] bits: BitBox, chunk_ranks: Box<[u32]>, @@ -43,7 +43,7 @@ type Chunk = u32; const CHUNK_SIZE: usize = Chunk::BITS as usize; impl RankBitBox { - pub(crate) fn from_bits(iter: impl Iterator) -> Self { + pub fn from_bits(iter: impl Iterator) -> Self { let bits: BitBox = iter.collect(); let chunk_ranks = bits .as_raw_slice() @@ -58,13 +58,13 @@ impl RankBitBox { } #[inline] - pub(crate) fn get_bit(&self, index: usize) -> Option { + pub fn get_bit(&self, index: usize) -> Option { self.bits.get(index).map(|bit| *bit) } /// Returns the number of bits _before_ (and not including) the given index that are set. #[inline] - pub(crate) fn rank(&self, index: usize) -> u32 { + pub fn rank(&self, index: usize) -> u32 { let chunk_index = index / CHUNK_SIZE; let index_within_chunk = index % CHUNK_SIZE; let chunk_rank = self.chunk_ranks[chunk_index]; diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index 82b470ff8c44a..1ff45782be9ea 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -35,17 +35,17 @@ pub mod ast_ids; mod builder; pub mod definition; pub mod expression; -pub(crate) mod member; -pub(crate) mod narrowing_constraints; +pub mod member; +pub mod narrowing_constraints; pub mod place; -pub(crate) mod predicate; +pub mod predicate; mod re_exports; mod reachability_constraints; -pub(crate) mod scope; -pub(crate) mod symbol; +pub mod scope; +pub mod symbol; mod use_def; -pub(crate) use self::use_def::{ +pub use self::use_def::{ ApplicableConstraints, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint, DeclarationsIterator, LiveBinding, }; @@ -54,7 +54,7 @@ pub(crate) use self::use_def::{ /// /// Prefer using [`symbol_table`] when working with symbols from a single scope. #[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { +pub fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { let _span = tracing::trace_span!("semantic_index", ?file).entered(); let module = parsed_module(db, file).load(db); @@ -68,7 +68,7 @@ pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { /// Salsa can avoid invalidating dependent queries if this scope's place table /// is unchanged. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { +pub fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { let file = scope.file(db); let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?file).entered(); let index = semantic_index(db, file); @@ -81,7 +81,7 @@ pub(crate) fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc(db: &'db dyn Db, file: File) -> Arc> { +pub fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc> { semantic_index(db, file).imported_modules.clone() } @@ -91,7 +91,7 @@ pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc> { +pub fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc> { let file = scope.file(db); let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?file).entered(); let index = semantic_index(db, file); @@ -133,22 +133,22 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc>, } impl LoopHeader { - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self { bindings: FxHashMap::default(), } } - pub(crate) fn add_binding(&mut self, place: ScopedPlaceId, binding: LiveBinding) { + pub fn add_binding(&mut self, place: ScopedPlaceId, binding: LiveBinding) { self.bindings.entry(place).or_default().push(binding); } - pub(crate) fn bindings_for_place( + pub fn bindings_for_place( &self, place: ScopedPlaceId, ) -> impl Iterator + '_ { @@ -183,7 +183,7 @@ impl get_size2::GetSize for LoopToken<'_> {} /// happens while we're building the semantic index, and nothing needs to call `get_loop_header` /// until we get to type inference later, so the order of operations always works out. #[salsa::tracked(specify, heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn get_loop_header<'db>(_db: &'db dyn Db, _loop_token: LoopToken<'db>) -> LoopHeader { +pub fn get_loop_header<'db>(_db: &'db dyn Db, _loop_token: LoopToken<'db>) -> LoopHeader { panic!("should always be set by specify()"); } @@ -192,7 +192,7 @@ pub(crate) fn get_loop_header<'db>(_db: &'db dyn Db, _loop_token: LoopToken<'db> /// /// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it /// introduces a direct dependency on that file's AST. -pub(crate) fn attribute_assignments<'db, 's>( +pub fn attribute_assignments<'db, 's>( db: &'db dyn Db, class_body_scope: ScopeId<'db>, name: &'s str, @@ -213,7 +213,7 @@ pub(crate) fn attribute_assignments<'db, 's>( /// /// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it /// introduces a direct dependency on that file's AST. -pub(crate) fn attribute_declarations<'db, 's>( +pub fn attribute_declarations<'db, 's>( db: &'db dyn Db, class_body_scope: ScopeId<'db>, name: &'s str, @@ -236,7 +236,7 @@ pub(crate) fn attribute_declarations<'db, 's>( /// /// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it /// introduces a direct dependency on that file's AST. -pub(crate) fn attribute_scopes<'db>( +pub fn attribute_scopes<'db>( db: &'db dyn Db, class_body_scope: ScopeId<'db>, ) -> impl Iterator + 'db { @@ -290,13 +290,13 @@ pub(crate) fn attribute_scopes<'db>( /// Returns the module global scope of `file`. #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> { +pub fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> { let _span = tracing::trace_span!("global_scope", ?file).entered(); FileScopeId::global().to_scope_id(db, file) } -pub(crate) enum EnclosingSnapshotResult<'map, 'db> { +pub enum EnclosingSnapshotResult<'map, 'db> { FoundConstraint(ScopedNarrowingConstraint), FoundBindings(BindingWithConstraintsIterator<'map, 'db>), NotFound, @@ -305,7 +305,7 @@ pub(crate) enum EnclosingSnapshotResult<'map, 'db> { /// The place tables and use-def maps for all scopes in a file. #[derive(Debug, Update, get_size2::GetSize)] -pub(crate) struct SemanticIndex<'db> { +pub struct SemanticIndex<'db> { /// List of all place tables in this file, indexed by scope. place_tables: IndexVec>, @@ -358,7 +358,7 @@ impl<'db> SemanticIndex<'db> { /// Use the Salsa cached [`place_table()`] query if you only need the /// place table for a single scope. #[track_caller] - pub(super) fn place_table(&self, scope_id: FileScopeId) -> &PlaceTable { + pub fn place_table(&self, scope_id: FileScopeId) -> &PlaceTable { &self.place_tables[scope_id] } @@ -367,18 +367,18 @@ impl<'db> SemanticIndex<'db> { /// Use the Salsa cached [`use_def_map()`] query if you only need the /// use-def map for a single scope. #[track_caller] - pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> &UseDefMap<'db> { + pub fn use_def_map(&self, scope_id: FileScopeId) -> &UseDefMap<'db> { &self.use_def_maps[scope_id] } #[track_caller] - pub(crate) fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds { + pub fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds { &self.ast_ids[scope_id] } /// Returns the ID of the `expression`'s enclosing scope. #[track_caller] - pub(crate) fn expression_scope_id(&self, expression: &E) -> FileScopeId + pub fn expression_scope_id(&self, expression: &E) -> FileScopeId where E: HasTrackedScope, { @@ -387,7 +387,7 @@ impl<'db> SemanticIndex<'db> { } /// Returns the ID of the `expression`'s enclosing scope. - pub(crate) fn try_expression_scope_id(&self, expression: &E) -> Option + pub fn try_expression_scope_id(&self, expression: &E) -> Option where E: HasTrackedScope, { @@ -397,38 +397,30 @@ impl<'db> SemanticIndex<'db> { /// Returns the [`Scope`] of the `expression`'s enclosing scope. #[allow(unused)] #[track_caller] - pub(crate) fn expression_scope(&self, expression: &impl HasTrackedScope) -> &Scope { + pub fn expression_scope(&self, expression: &impl HasTrackedScope) -> &Scope { &self.scopes[self.expression_scope_id(expression)] } /// Returns the [`Scope`] with the given id. #[track_caller] - pub(crate) fn scope(&self, id: FileScopeId) -> &Scope { + pub fn scope(&self, id: FileScopeId) -> &Scope { &self.scopes[id] } - pub(crate) fn scope_ids(&self) -> impl Iterator> + '_ { + pub fn scope_ids(&self) -> impl Iterator> + '_ { self.scope_ids_by_scope.iter().copied() } - pub(crate) fn symbol_is_global_in_scope( - &self, - symbol: ScopedSymbolId, - scope: FileScopeId, - ) -> bool { + pub fn symbol_is_global_in_scope(&self, symbol: ScopedSymbolId, scope: FileScopeId) -> bool { self.place_table(scope).symbol(symbol).is_global() } - pub(crate) fn symbol_is_nonlocal_in_scope( - &self, - symbol: ScopedSymbolId, - scope: FileScopeId, - ) -> bool { + pub fn symbol_is_nonlocal_in_scope(&self, symbol: ScopedSymbolId, scope: FileScopeId) -> bool { self.place_table(scope).symbol(symbol).is_nonlocal() } /// Returns the id of the parent scope. - pub(crate) fn parent_scope_id(&self, scope_id: FileScopeId) -> Option { + pub fn parent_scope_id(&self, scope_id: FileScopeId) -> Option { let scope = self.scope(scope_id); scope.parent() } @@ -436,13 +428,13 @@ impl<'db> SemanticIndex<'db> { /// Returns the parent scope of `scope_id`. #[expect(unused)] #[track_caller] - pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> { + pub fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> { Some(&self.scopes[self.parent_scope_id(scope_id)?]) } /// Return the [`Definition`] of the class enclosing this method, given the /// method's body scope, or `None` if it is not a method. - pub(crate) fn class_definition_of_method( + pub fn class_definition_of_method( &self, function_body_scope: FileScopeId, ) -> Option> { @@ -473,7 +465,7 @@ impl<'db> SemanticIndex<'db> { .map(|node_ref| self.expect_single_definition(node_ref)) } - pub(crate) fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool { + pub fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool { self.parent_scope_id(scope_id) .is_none_or(|parent_scope_id| { if !self.is_scope_reachable(db, parent_scope_id) { @@ -498,7 +490,7 @@ impl<'db> SemanticIndex<'db> { /// return /// x # 3 /// ``` - pub(crate) fn is_node_reachable( + pub fn is_node_reachable( &self, db: &'db dyn crate::Db, scope_id: FileScopeId, @@ -510,18 +502,18 @@ impl<'db> SemanticIndex<'db> { /// Returns an iterator over the descendent scopes of `scope`. #[allow(unused)] - pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter<'_> { + pub fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter<'_> { DescendantsIter::new(&self.scopes, scope) } /// Returns an iterator over the direct child scopes of `scope`. #[allow(unused)] - pub(crate) fn child_scopes(&self, scope: FileScopeId) -> ChildrenIter<'_> { + pub fn child_scopes(&self, scope: FileScopeId) -> ChildrenIter<'_> { ChildrenIter::new(&self.scopes, scope) } /// Returns an iterator over all ancestors of `scope`, starting with `scope` itself. - pub(crate) fn ancestor_scopes(&self, scope: FileScopeId) -> AncestorsIter<'_> { + pub fn ancestor_scopes(&self, scope: FileScopeId) -> AncestorsIter<'_> { AncestorsIter::new(&self.scopes, scope) } @@ -539,7 +531,7 @@ impl<'db> SemanticIndex<'db> { /// print(x) # Refers to global x=1, not class x=2 /// ``` /// The `method` function can see the global scope but not the class scope. - pub(crate) fn visible_ancestor_scopes(&self, scope: FileScopeId) -> VisibleAncestorsIter<'_> { + pub fn visible_ancestor_scopes(&self, scope: FileScopeId) -> VisibleAncestorsIter<'_> { VisibleAncestorsIter::new(&self.scopes, scope) } @@ -548,10 +540,7 @@ impl<'db> SemanticIndex<'db> { /// There will only ever be >1 `Definition` associated with a `definition_key` /// if the definition is created by a wildcard (`*`) import. #[track_caller] - pub(crate) fn definitions( - &self, - definition_key: impl Into, - ) -> &Definitions<'db> { + pub fn definitions(&self, definition_key: impl Into) -> &Definitions<'db> { &self.definitions_by_node[&definition_key.into()] } @@ -567,7 +556,7 @@ impl<'db> SemanticIndex<'db> { /// situations that can result in multiple definitions being associated with a /// single AST node. #[track_caller] - pub(crate) fn expect_single_definition( + pub fn expect_single_definition( &self, definition_key: impl Into + std::fmt::Debug + Copy, ) -> Definition<'db> { @@ -586,14 +575,11 @@ impl<'db> SemanticIndex<'db> { /// standalone-inferable expressions, which we call `add_standalone_expression` for in /// [`SemanticIndexBuilder`]. #[track_caller] - pub(crate) fn expression( - &self, - expression_key: impl Into, - ) -> Expression<'db> { + pub fn expression(&self, expression_key: impl Into) -> Expression<'db> { self.expressions_by_node[&expression_key.into()] } - pub(crate) fn try_expression( + pub fn try_expression( &self, expression_key: impl Into, ) -> Option> { @@ -602,10 +588,7 @@ impl<'db> SemanticIndex<'db> { .copied() } - pub(crate) fn is_standalone_expression( - &self, - expression_key: impl Into, - ) -> bool { + pub fn is_standalone_expression(&self, expression_key: impl Into) -> bool { self.expressions_by_node .contains_key(&expression_key.into()) } @@ -614,12 +597,12 @@ impl<'db> SemanticIndex<'db> { /// This is different from [`definition::Definition::scope`] which /// returns the scope in which that definition is defined in. #[track_caller] - pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId { + pub fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId { self.scopes_by_node[&node.node_key()] } /// Returns the id of the scope that `node` creates, if it exists. - pub(crate) fn try_node_scope(&self, node: NodeWithScopeRef) -> Option { + pub fn try_node_scope(&self, node: NodeWithScopeRef) -> Option { self.scopes_by_node.get(&node.node_key()).copied() } @@ -628,13 +611,13 @@ impl<'db> SemanticIndex<'db> { /// This is useful when you have a [`NodeWithScopeKey`] constructed from an /// [`AstNodeRef`](crate::ast_node_ref::AstNodeRef) and want to avoid loading /// the parsed module just to look up the scope. - pub(crate) fn node_scope_by_key(&self, key: NodeWithScopeKey) -> FileScopeId { + pub fn node_scope_by_key(&self, key: NodeWithScopeKey) -> FileScopeId { self.scopes_by_node[&key] } /// Checks if there is an import of `__future__.annotations` in the global scope, which affects /// the logic for type inference. - pub(super) fn has_future_annotations(&self) -> bool { + pub fn has_future_annotations(&self) -> bool { self.has_future_annotations } @@ -644,7 +627,7 @@ impl<'db> SemanticIndex<'db> { /// * an iterator of bindings for a particular nested scope reference if the bindings exist. /// * a narrowing constraint if there are no bindings, but there is a narrowing constraint for an enclosing scope place. /// * `NotFound` if the narrowing constraint / bindings do not exist in the nested scope. - pub(crate) fn enclosing_snapshot( + pub fn enclosing_snapshot( &self, enclosing_scope: FileScopeId, expr: PlaceExprRef, @@ -688,12 +671,12 @@ impl<'db> SemanticIndex<'db> { self.use_def_maps[enclosing_scope].enclosing_snapshot(*id, key.nested_laziness) } - pub(crate) fn semantic_syntax_errors(&self) -> &[SemanticSyntaxError] { + pub fn semantic_syntax_errors(&self) -> &[SemanticSyntaxError] { &self.semantic_syntax_errors } } -pub(crate) struct AncestorsIter<'a> { +pub struct AncestorsIter<'a> { scopes: &'a IndexSlice, next_id: Option, } @@ -721,7 +704,7 @@ impl<'a> Iterator for AncestorsIter<'a> { impl FusedIterator for AncestorsIter<'_> {} -pub(crate) struct VisibleAncestorsIter<'a> { +pub struct VisibleAncestorsIter<'a> { inner: AncestorsIter<'a>, starting_scope_kind: ScopeKind, yielded_count: usize, @@ -768,7 +751,7 @@ impl<'a> Iterator for VisibleAncestorsIter<'a> { impl FusedIterator for VisibleAncestorsIter<'_> {} -pub(crate) struct DescendantsIter<'a> { +pub struct DescendantsIter<'a> { next_id: FileScopeId, descendants: std::slice::Iter<'a, Scope>, } @@ -805,13 +788,13 @@ impl FusedIterator for DescendantsIter<'_> {} impl ExactSizeIterator for DescendantsIter<'_> {} -pub(crate) struct ChildrenIter<'a> { +pub struct ChildrenIter<'a> { parent: FileScopeId, descendants: DescendantsIter<'a>, } impl<'a> ChildrenIter<'a> { - pub(crate) fn new(scopes: &'a IndexSlice, parent: FileScopeId) -> Self { + pub fn new(scopes: &'a IndexSlice, parent: FileScopeId) -> Self { let descendants = DescendantsIter::new(scopes, parent); Self { diff --git a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs index 5b8e83a2b0e4d..be5004bcb6991 100644 --- a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs +++ b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs @@ -25,7 +25,7 @@ use crate::semantic_index::semantic_index; /// x = foo() /// ``` #[derive(Debug, salsa::Update, get_size2::GetSize)] -pub(crate) struct AstIds { +pub struct AstIds { /// Maps expressions which "use" a place (that is, [`ast::ExprName`], [`ast::ExprAttribute`] or [`ast::ExprSubscript`]) to a use id. uses_map: FxHashMap, } @@ -86,13 +86,13 @@ impl HasScopedUseId for ast::ExprRef<'_> { } #[derive(Debug, Default)] -pub(super) struct AstIdsBuilder { +pub struct AstIdsBuilder { uses_map: FxHashMap, } impl AstIdsBuilder { /// Adds `expr` to the use ids map and returns its id. - pub(super) fn record_use(&mut self, expr: impl Into) -> ScopedUseId { + pub fn record_use(&mut self, expr: impl Into) -> ScopedUseId { let use_id = self.uses_map.len().into(); self.uses_map.insert(expr.into(), use_id); @@ -100,7 +100,7 @@ impl AstIdsBuilder { use_id } - pub(super) fn finish(mut self) -> AstIds { + pub fn finish(mut self) -> AstIds { self.uses_map.shrink_to_fit(); AstIds { @@ -110,13 +110,13 @@ impl AstIdsBuilder { } /// Node key that can only be constructed for expressions. -pub(crate) mod node_key { +pub mod node_key { use ruff_python_ast as ast; use crate::node_key::NodeKey; #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update, get_size2::GetSize)] - pub(crate) struct ExpressionNodeKey(NodeKey); + pub struct ExpressionNodeKey(NodeKey); impl From> for ExpressionNodeKey { fn from(value: ast::ExprRef<'_>) -> Self { diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index ea24d8594a6f0..1f34b1917cba4 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -87,7 +87,7 @@ struct ScopeInfo { current_loop: Option, } -pub(super) struct SemanticIndexBuilder<'db, 'ast> { +pub struct SemanticIndexBuilder<'db, 'ast> { // Builder state db: &'db dyn Db, file: File, @@ -139,7 +139,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { } impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { - pub(super) fn new(db: &'db dyn Db, file: File, module_ref: &'ast ParsedModuleRef) -> Self { + pub fn new(db: &'db dyn Db, file: File, module_ref: &'ast ParsedModuleRef) -> Self { let mut builder = Self { db, file, @@ -1544,7 +1544,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } } - pub(super) fn build(mut self) -> SemanticIndex<'db> { + pub fn build(mut self) -> SemanticIndex<'db> { self.visit_body(self.module.suite()); // Pop the root scope diff --git a/crates/ty_python_semantic/src/semantic_index/builder/except_handlers.rs b/crates/ty_python_semantic/src/semantic_index/builder/except_handlers.rs index 6438d1996f3d1..4fd9e4b081657 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder/except_handlers.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder/except_handlers.rs @@ -4,20 +4,20 @@ use super::SemanticIndexBuilder; /// An abstraction over the fact that each scope should have its own [`TryNodeContextStack`] #[derive(Debug, Default)] -pub(super) struct TryNodeContextStackManager(Vec); +pub struct TryNodeContextStackManager(Vec); impl TryNodeContextStackManager { /// Push a new [`TryNodeContextStack`] onto the stack of stacks. /// /// Each [`TryNodeContextStack`] is only valid for a single scope - pub(super) fn enter_nested_scope(&mut self) { + pub fn enter_nested_scope(&mut self) { self.0.push(TryNodeContextStack::default()); } /// Pop a new [`TryNodeContextStack`] off the stack of stacks. /// /// Each [`TryNodeContextStack`] is only valid for a single scope - pub(super) fn exit_scope(&mut self) { + pub fn exit_scope(&mut self) { let popped_context = self.0.pop(); debug_assert!( popped_context.is_some(), @@ -28,20 +28,20 @@ impl TryNodeContextStackManager { /// Push a [`TryNodeContext`] onto the [`TryNodeContextStack`] /// at the top of our stack of stacks - pub(super) fn push_context(&mut self) { + pub fn push_context(&mut self) { self.current_try_context_stack().push_context(); } /// Pop a [`TryNodeContext`] off the [`TryNodeContextStack`] /// at the top of our stack of stacks. Return the Vec of [`FlowSnapshot`]s /// recorded while we were visiting the `try` suite. - pub(super) fn pop_context(&mut self) -> Vec { + pub fn pop_context(&mut self) -> Vec { self.current_try_context_stack().pop_context() } /// Retrieve the stack that is at the top of our stack of stacks. /// For each `try` block on that stack, push the snapshot onto the `try` block - pub(super) fn record_definition(&mut self, builder: &SemanticIndexBuilder) { + pub fn record_definition(&mut self, builder: &SemanticIndexBuilder) { self.current_try_context_stack().record_definition(builder); } diff --git a/crates/ty_python_semantic/src/semantic_index/builder/loop_bindings_visitor.rs b/crates/ty_python_semantic/src/semantic_index/builder/loop_bindings_visitor.rs index 74918572a81b4..4a81ef1644c7f 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder/loop_bindings_visitor.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder/loop_bindings_visitor.rs @@ -10,7 +10,7 @@ use crate::semantic_index::symbol::Symbol; /// pre-walk so that we can synthesize "loop header definitions" that are visible to the loop body /// (and condition). See `LoopHeader`. /// TODO: Handle `nonlocal` bindings from nested scopes somehow. -pub(crate) fn collect_while_loop_bindings(while_stmt: &ast::StmtWhile) -> Vec { +pub fn collect_while_loop_bindings(while_stmt: &ast::StmtWhile) -> Vec { let mut collector = LoopBindingsVisitor::default(); collector.visit_expr(&while_stmt.test); collector.visit_body(&while_stmt.body); @@ -18,7 +18,7 @@ pub(crate) fn collect_while_loop_bindings(while_stmt: &ast::StmtWhile) -> Vec Vec { +pub fn collect_for_loop_bindings(for_stmt: &ast::StmtFor) -> Vec { let mut collector = LoopBindingsVisitor::default(); collector.add_place_from_target(&for_stmt.target); collector.visit_body(&for_stmt.body); @@ -29,12 +29,12 @@ pub(crate) fn collect_for_loop_bindings(for_stmt: &ast::StmtFor) -> Vec, } impl LoopBindingsVisitor { - pub(crate) fn add_place_from_target(&mut self, target: &ast::Expr) { + pub fn add_place_from_target(&mut self, target: &ast::Expr) { match target { ast::Expr::Name(name) => { self.bound_places.push(PlaceExpr::from_expr_name(name)); diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 26c570e6458c8..eb465adb22fe9 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -31,10 +31,10 @@ pub struct Definition<'db> { pub file: File, /// The scope in which the definition occurs. - pub(crate) file_scope: FileScopeId, + pub file_scope: FileScopeId, /// The place ID of the definition. - pub(crate) place: ScopedPlaceId, + pub place: ScopedPlaceId, /// WARNING: Only access this field when doing type inference for the same /// file as where `Definition` is defined to avoid cross-file query dependencies. @@ -44,14 +44,14 @@ pub struct Definition<'db> { pub kind: DefinitionKind<'db>, /// This is a dedicated field to avoid accessing `kind` to compute this value. - pub(crate) is_reexported: bool, + pub is_reexported: bool, } // The Salsa heap is tracked separately. impl get_size2::GetSize for Definition<'_> {} impl<'db> Definition<'db> { - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { self.file_scope(db).to_scope_id(db, self.file(db)) } @@ -133,7 +133,7 @@ impl<'db> Definition<'db> { } /// Get the module-level docstring for the given file. -pub(crate) fn module_docstring(db: &dyn Db, file: File) -> Option { +pub fn module_docstring(db: &dyn Db, file: File) -> Option { let module = parsed_module(db, file).load(db); docstring_from_body(module.suite()) .map(|docstring_expr| docstring_expr.value.to_str().to_owned()) @@ -200,13 +200,13 @@ pub struct Definitions<'db> { } impl<'db> Definitions<'db> { - pub(crate) fn single(definition: Definition<'db>) -> Self { + pub fn single(definition: Definition<'db>) -> Self { Self { definitions: smallvec::smallvec_inline![definition], } } - pub(crate) fn push(&mut self, definition: Definition<'db>) { + pub fn push(&mut self, definition: Definition<'db>) { self.definitions.push(definition); } } @@ -229,7 +229,7 @@ impl<'a, 'db> IntoIterator for &'a Definitions<'db> { } #[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) enum DefinitionState<'db> { +pub enum DefinitionState<'db> { Defined(Definition<'db>), /// Represents the implicit "unbound"/"undeclared" definition of every place. Undefined, @@ -239,17 +239,17 @@ pub(crate) enum DefinitionState<'db> { } impl<'db> DefinitionState<'db> { - pub(crate) fn is_defined_and(self, f: impl Fn(Definition<'db>) -> bool) -> bool { + pub fn is_defined_and(self, f: impl Fn(Definition<'db>) -> bool) -> bool { matches!(self, DefinitionState::Defined(def) if f(def)) } - pub(crate) fn is_undefined_or(self, f: impl Fn(Definition<'db>) -> bool) -> bool { + pub fn is_undefined_or(self, f: impl Fn(Definition<'db>) -> bool) -> bool { matches!(self, DefinitionState::Undefined) || matches!(self, DefinitionState::Defined(def) if f(def)) } #[allow(unused)] - pub(crate) fn definition(self) -> Option> { + pub fn definition(self) -> Option> { match self { DefinitionState::Defined(def) => Some(def), DefinitionState::Deleted | DefinitionState::Undefined => None, @@ -258,7 +258,7 @@ impl<'db> DefinitionState<'db> { } #[derive(Copy, Clone, Debug)] -pub(crate) enum DefinitionNodeRef<'ast, 'db> { +pub enum DefinitionNodeRef<'ast, 'db> { Import(ImportDefinitionNodeRef<'ast>), ImportFrom(ImportFromDefinitionNodeRef<'ast>), ImportFromSubmodule(ImportFromSubmoduleDefinitionNodeRef<'ast>), @@ -412,111 +412,111 @@ impl<'ast> From> for DefinitionNodeRef<'ast, ' } #[derive(Copy, Clone, Debug)] -pub(crate) struct ImportDefinitionNodeRef<'ast> { - pub(crate) node: &'ast ast::StmtImport, - pub(crate) alias_index: usize, - pub(crate) is_reexported: bool, +pub struct ImportDefinitionNodeRef<'ast> { + pub node: &'ast ast::StmtImport, + pub alias_index: usize, + pub is_reexported: bool, } #[derive(Copy, Clone, Debug)] -pub(crate) struct StarImportDefinitionNodeRef<'ast> { - pub(crate) node: &'ast ast::StmtImportFrom, - pub(crate) symbol_id: ScopedSymbolId, +pub struct StarImportDefinitionNodeRef<'ast> { + pub node: &'ast ast::StmtImportFrom, + pub symbol_id: ScopedSymbolId, } #[derive(Copy, Clone, Debug)] -pub(crate) struct ImportFromDefinitionNodeRef<'ast> { - pub(crate) node: &'ast ast::StmtImportFrom, - pub(crate) alias_index: usize, - pub(crate) is_reexported: bool, +pub struct ImportFromDefinitionNodeRef<'ast> { + pub node: &'ast ast::StmtImportFrom, + pub alias_index: usize, + pub is_reexported: bool, } #[derive(Copy, Clone, Debug)] -pub(crate) struct ImportFromSubmoduleDefinitionNodeRef<'ast> { - pub(crate) node: &'ast ast::StmtImportFrom, - pub(crate) module: &'ast ast::Identifier, - pub(crate) module_index: usize, +pub struct ImportFromSubmoduleDefinitionNodeRef<'ast> { + pub node: &'ast ast::StmtImportFrom, + pub module: &'ast ast::Identifier, + pub module_index: usize, } #[derive(Copy, Clone, Debug)] -pub(crate) struct AssignmentDefinitionNodeRef<'ast, 'db> { - pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>, - pub(crate) value: &'ast ast::Expr, - pub(crate) target: &'ast ast::Expr, +pub struct AssignmentDefinitionNodeRef<'ast, 'db> { + pub unpack: Option<(UnpackPosition, Unpack<'db>)>, + pub value: &'ast ast::Expr, + pub target: &'ast ast::Expr, } #[derive(Copy, Clone, Debug)] -pub(crate) struct AnnotatedAssignmentDefinitionNodeRef<'ast> { - pub(crate) node: &'ast ast::StmtAnnAssign, - pub(crate) annotation: &'ast ast::Expr, - pub(crate) value: Option<&'ast ast::Expr>, - pub(crate) target: &'ast ast::Expr, +pub struct AnnotatedAssignmentDefinitionNodeRef<'ast> { + pub node: &'ast ast::StmtAnnAssign, + pub annotation: &'ast ast::Expr, + pub value: Option<&'ast ast::Expr>, + pub target: &'ast ast::Expr, } #[derive(Copy, Clone, Debug)] -pub(crate) struct DictKeyAssignmentNodeRef<'ast, 'db> { - pub(crate) key: &'ast ast::Expr, - pub(crate) value: &'ast ast::Expr, - pub(crate) assignment: Definition<'db>, +pub struct DictKeyAssignmentNodeRef<'ast, 'db> { + pub key: &'ast ast::Expr, + pub value: &'ast ast::Expr, + pub assignment: Definition<'db>, } #[derive(Copy, Clone, Debug)] -pub(crate) struct WithItemDefinitionNodeRef<'ast, 'db> { - pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>, - pub(crate) context_expr: &'ast ast::Expr, - pub(crate) target: &'ast ast::Expr, - pub(crate) is_async: bool, +pub struct WithItemDefinitionNodeRef<'ast, 'db> { + pub unpack: Option<(UnpackPosition, Unpack<'db>)>, + pub context_expr: &'ast ast::Expr, + pub target: &'ast ast::Expr, + pub is_async: bool, } #[derive(Copy, Clone, Debug)] -pub(crate) struct ForStmtDefinitionNodeRef<'ast, 'db> { - pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>, - pub(crate) iterable: &'ast ast::Expr, - pub(crate) target: &'ast ast::Expr, - pub(crate) is_async: bool, +pub struct ForStmtDefinitionNodeRef<'ast, 'db> { + pub unpack: Option<(UnpackPosition, Unpack<'db>)>, + pub iterable: &'ast ast::Expr, + pub target: &'ast ast::Expr, + pub is_async: bool, } #[derive(Copy, Clone, Debug)] -pub(crate) struct ExceptHandlerDefinitionNodeRef<'ast> { - pub(crate) handler: &'ast ast::ExceptHandlerExceptHandler, - pub(crate) is_star: bool, +pub struct ExceptHandlerDefinitionNodeRef<'ast> { + pub handler: &'ast ast::ExceptHandlerExceptHandler, + pub is_star: bool, } #[derive(Copy, Clone, Debug)] -pub(crate) struct LoopHeaderDefinitionNodeRef<'ast, 'db> { - pub(crate) loop_stmt: LoopStmtRef<'ast>, - pub(crate) place: ScopedPlaceId, - pub(crate) loop_token: LoopToken<'db>, +pub struct LoopHeaderDefinitionNodeRef<'ast, 'db> { + pub loop_stmt: LoopStmtRef<'ast>, + pub place: ScopedPlaceId, + pub loop_token: LoopToken<'db>, } #[derive(Copy, Clone, Debug)] -pub(crate) enum LoopStmtRef<'ast> { +pub enum LoopStmtRef<'ast> { While(&'ast ast::StmtWhile), For(&'ast ast::StmtFor), } #[derive(Copy, Clone, Debug)] -pub(crate) struct ComprehensionDefinitionNodeRef<'ast, 'db> { - pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>, - pub(crate) iterable: &'ast ast::Expr, - pub(crate) target: &'ast ast::Expr, - pub(crate) first: bool, - pub(crate) is_async: bool, +pub struct ComprehensionDefinitionNodeRef<'ast, 'db> { + pub unpack: Option<(UnpackPosition, Unpack<'db>)>, + pub iterable: &'ast ast::Expr, + pub target: &'ast ast::Expr, + pub first: bool, + pub is_async: bool, } #[derive(Copy, Clone, Debug)] -pub(crate) struct MatchPatternDefinitionNodeRef<'ast> { +pub struct MatchPatternDefinitionNodeRef<'ast> { /// The outermost pattern node in which the identifier being defined occurs. - pub(crate) pattern: &'ast ast::Pattern, + pub pattern: &'ast ast::Pattern, /// The identifier being defined. - pub(crate) identifier: &'ast ast::Identifier, + pub identifier: &'ast ast::Identifier, /// The index of the identifier in the pattern when visiting the `pattern` node in evaluation /// order. - pub(crate) index: u32, + pub index: u32, } impl<'db> DefinitionNodeRef<'_, 'db> { - pub(super) fn into_owned(self, parsed: &ParsedModuleRef) -> DefinitionKind<'db> { + pub fn into_owned(self, parsed: &ParsedModuleRef) -> DefinitionKind<'db> { match self { DefinitionNodeRef::Import(ImportDefinitionNodeRef { node, @@ -679,7 +679,7 @@ impl<'db> DefinitionNodeRef<'_, 'db> { } } - pub(super) fn key(self) -> DefinitionNodeKey { + pub fn key(self) -> DefinitionNodeKey { match self { Self::Import(ImportDefinitionNodeRef { node, @@ -752,7 +752,7 @@ impl<'db> DefinitionNodeRef<'_, 'db> { } #[derive(Clone, Copy, Debug)] -pub(crate) enum DefinitionCategory { +pub enum DefinitionCategory { /// A Definition which binds a value to a name (e.g. `x = 1`). Binding, /// A Definition which declares the upper-bound of acceptable types for this name (`x: int`). @@ -768,7 +768,7 @@ impl DefinitionCategory { /// type not assignable to the declared type. /// /// Annotations establish a declared type. So do function and class definitions, and imports. - pub(crate) fn is_declaration(self) -> bool { + pub fn is_declaration(self) -> bool { matches!( self, DefinitionCategory::Declaration | DefinitionCategory::DeclarationAndBinding @@ -778,7 +778,7 @@ impl DefinitionCategory { /// True if this definition assigns a value to the place. /// /// False only for annotated assignments without a RHS. - pub(crate) fn is_binding(self) -> bool { + pub fn is_binding(self) -> bool { matches!( self, DefinitionCategory::Binding | DefinitionCategory::DeclarationAndBinding @@ -822,7 +822,7 @@ pub enum DefinitionKind<'db> { } impl DefinitionKind<'_> { - pub(crate) fn is_reexported(&self) -> bool { + pub fn is_reexported(&self) -> bool { match self { DefinitionKind::Import(import) => import.is_reexported(), DefinitionKind::ImportFrom(import) => import.is_reexported(), @@ -831,21 +831,21 @@ impl DefinitionKind<'_> { } } - pub(crate) const fn as_star_import(&self) -> Option<&StarImportDefinitionKind> { + pub const fn as_star_import(&self) -> Option<&StarImportDefinitionKind> { match self { DefinitionKind::StarImport(import) => Some(import), _ => None, } } - pub(crate) const fn as_class(&self) -> Option<&AstNodeRef> { + pub const fn as_class(&self) -> Option<&AstNodeRef> { match self { DefinitionKind::Class(class) => Some(class), _ => None, } } - pub(crate) fn is_import(&self) -> bool { + pub fn is_import(&self) -> bool { matches!( self, DefinitionKind::Import(_) @@ -855,21 +855,21 @@ impl DefinitionKind<'_> { ) } - pub(crate) const fn is_unannotated_assignment(&self) -> bool { + pub const fn is_unannotated_assignment(&self) -> bool { matches!(self, DefinitionKind::Assignment(_)) } - pub(crate) const fn is_function_def(&self) -> bool { + pub const fn is_function_def(&self) -> bool { matches!(self, DefinitionKind::Function(_)) } - pub(crate) const fn is_loop_header(&self) -> bool { + pub const fn is_loop_header(&self) -> bool { matches!(self, DefinitionKind::LoopHeader(_)) } /// Returns `true` if this definition is user-visible (i.e., not an internal /// control-flow construct like a loop header definition). - pub(crate) const fn is_user_visible(&self) -> bool { + pub const fn is_user_visible(&self) -> bool { !self.is_loop_header() } @@ -877,7 +877,7 @@ impl DefinitionKind<'_> { /// /// A definition target would mainly be the node representing the place being defined i.e., /// [`ast::ExprName`], [`ast::Identifier`], [`ast::ExprAttribute`] or [`ast::ExprSubscript`] but could also be other nodes. - pub(crate) fn target_range(&self, module: &ParsedModuleRef) -> TextRange { + pub fn target_range(&self, module: &ParsedModuleRef) -> TextRange { match self { DefinitionKind::Import(import) => import.alias(module).range(), DefinitionKind::ImportFrom(import) => import.alias(module).range(), @@ -919,7 +919,7 @@ impl DefinitionKind<'_> { } /// Returns the [`TextRange`] of the entire definition. - pub(crate) fn full_range(&self, module: &ParsedModuleRef) -> TextRange { + pub fn full_range(&self, module: &ParsedModuleRef) -> TextRange { match self { DefinitionKind::Import(import) => import.alias(module).range(), DefinitionKind::ImportFrom(import) => import.alias(module).range(), @@ -967,7 +967,7 @@ impl DefinitionKind<'_> { } } - pub(crate) fn category(&self, in_stub: bool, module: &ParsedModuleRef) -> DefinitionCategory { + pub fn category(&self, in_stub: bool, module: &ParsedModuleRef) -> DefinitionCategory { match self { // functions, classes, and imports always bind, and we consider them declarations DefinitionKind::Function(_) @@ -1029,7 +1029,7 @@ impl DefinitionKind<'_> { /// /// Returns `Some` for `Assignment` and `AnnotatedAssignment` (if it has a value), /// `None` for all other definition kinds. - pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> Option<&'ast ast::Expr> { + pub fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> Option<&'ast ast::Expr> { match self { DefinitionKind::Assignment(assignment) => Some(assignment.value(module)), DefinitionKind::AnnotatedAssignment(assignment) => assignment.value(module), @@ -1039,7 +1039,7 @@ impl DefinitionKind<'_> { } #[derive(Copy, Clone, Debug, PartialEq, Hash, get_size2::GetSize)] -pub(crate) enum TargetKind<'db> { +pub enum TargetKind<'db> { Sequence(UnpackPosition, Unpack<'db>), /// Name, attribute, or subscript. Single, @@ -1065,7 +1065,7 @@ impl StarImportDefinitionKind { self.node.node(module) } - pub(crate) fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Alias { + pub fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Alias { // INVARIANT: for an invalid-syntax statement such as `from foo import *, bar, *`, // we only create a `StarImportDefinitionKind` for the *first* `*` alias in the names list. self.node @@ -1079,7 +1079,7 @@ impl StarImportDefinitionKind { ) } - pub(crate) fn symbol_id(&self) -> ScopedSymbolId { + pub fn symbol_id(&self) -> ScopedSymbolId { self.symbol_id } } @@ -1092,11 +1092,11 @@ pub struct MatchPatternDefinitionKind { } impl MatchPatternDefinitionKind { - pub(crate) fn pattern<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Pattern { + pub fn pattern<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Pattern { self.pattern.node(module) } - pub(crate) fn index(&self) -> u32 { + pub fn index(&self) -> u32 { self.index } } @@ -1116,23 +1116,23 @@ pub struct ComprehensionDefinitionKind<'db> { } impl<'db> ComprehensionDefinitionKind<'db> { - pub(crate) fn iterable<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn iterable<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.iterable.node(module) } - pub(crate) fn target_kind(&self) -> TargetKind<'db> { + pub fn target_kind(&self) -> TargetKind<'db> { self.target_kind } - pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.target.node(module) } - pub(crate) fn is_first(&self) -> bool { + pub fn is_first(&self) -> bool { self.first } - pub(crate) fn is_async(&self) -> bool { + pub fn is_async(&self) -> bool { self.is_async } } @@ -1149,11 +1149,11 @@ impl ImportDefinitionKind { self.node.node(module) } - pub(crate) fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Alias { + pub fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Alias { &self.node.node(module).names[self.alias_index] } - pub(crate) fn is_reexported(&self) -> bool { + pub fn is_reexported(&self) -> bool { self.is_reexported } } @@ -1170,11 +1170,11 @@ impl ImportFromDefinitionKind { self.node.node(module) } - pub(crate) fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Alias { + pub fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Alias { &self.node.node(module).names[self.alias_index] } - pub(crate) fn is_reexported(&self) -> bool { + pub fn is_reexported(&self) -> bool { self.is_reexported } } @@ -1225,7 +1225,7 @@ pub struct AssignmentDefinitionKind<'db> { } impl<'db> AssignmentDefinitionKind<'db> { - pub(crate) fn target_kind(&self) -> TargetKind<'db> { + pub fn target_kind(&self) -> TargetKind<'db> { self.target_kind } @@ -1233,7 +1233,7 @@ impl<'db> AssignmentDefinitionKind<'db> { self.value.node(module) } - pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.target.node(module) } } @@ -1246,32 +1246,32 @@ pub struct AnnotatedAssignmentDefinitionKind { } impl AnnotatedAssignmentDefinitionKind { - pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> Option<&'ast ast::Expr> { + pub fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> Option<&'ast ast::Expr> { self.value.as_ref().map(|value| value.node(module)) } - pub(crate) fn annotation<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn annotation<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.annotation.node(module) } - pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.target.node(module) } } #[derive(Clone, Debug, get_size2::GetSize)] pub struct DictKeyAssignmentKind<'db> { - pub(crate) key: AstNodeRef, - pub(crate) value: AstNodeRef, - pub(crate) assignment: Definition<'db>, + pub key: AstNodeRef, + pub value: AstNodeRef, + pub assignment: Definition<'db>, } impl DictKeyAssignmentKind<'_> { - pub(crate) fn key<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn key<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.key.node(module) } - pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.value.node(module) } } @@ -1285,19 +1285,19 @@ pub struct WithItemDefinitionKind<'db> { } impl<'db> WithItemDefinitionKind<'db> { - pub(crate) fn context_expr<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn context_expr<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.context_expr.node(module) } - pub(crate) fn target_kind(&self) -> TargetKind<'db> { + pub fn target_kind(&self) -> TargetKind<'db> { self.target_kind } - pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.target.node(module) } - pub(crate) const fn is_async(&self) -> bool { + pub const fn is_async(&self) -> bool { self.is_async } } @@ -1311,19 +1311,19 @@ pub struct ForStmtDefinitionKind<'db> { } impl<'db> ForStmtDefinitionKind<'db> { - pub(crate) fn iterable<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn iterable<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.iterable.node(module) } - pub(crate) fn target_kind(&self) -> TargetKind<'db> { + pub fn target_kind(&self) -> TargetKind<'db> { self.target_kind } - pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + pub fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { self.target.node(module) } - pub(crate) const fn is_async(&self) -> bool { + pub const fn is_async(&self) -> bool { self.is_async } } @@ -1335,21 +1335,21 @@ pub struct ExceptHandlerDefinitionKind { } impl ExceptHandlerDefinitionKind { - pub(crate) fn node<'ast>( + pub fn node<'ast>( &self, module: &'ast ParsedModuleRef, ) -> &'ast ast::ExceptHandlerExceptHandler { self.handler.node(module) } - pub(crate) fn handled_exceptions<'ast>( + pub fn handled_exceptions<'ast>( &self, module: &'ast ParsedModuleRef, ) -> Option<&'ast ast::Expr> { self.node(module).type_.as_deref() } - pub(crate) fn is_star(&self) -> bool { + pub fn is_star(&self) -> bool { self.is_star } } @@ -1365,21 +1365,21 @@ pub struct LoopHeaderDefinitionKind<'db> { } #[derive(Clone, Debug, get_size2::GetSize)] -pub(crate) enum LoopStmtKind { +pub enum LoopStmtKind { While(AstNodeRef), For(AstNodeRef), } impl<'db> LoopHeaderDefinitionKind<'db> { - pub(crate) fn loop_token(&self) -> LoopToken<'db> { + pub fn loop_token(&self) -> LoopToken<'db> { self.loop_token } - pub(crate) fn place(&self) -> ScopedPlaceId { + pub fn place(&self) -> ScopedPlaceId { self.place } - pub(crate) fn range(&self, module: &ParsedModuleRef) -> TextRange { + pub fn range(&self, module: &ParsedModuleRef) -> TextRange { match &self.loop_stmt { LoopStmtKind::While(stmt) => stmt.node(module).range(), LoopStmtKind::For(stmt) => stmt.node(module).range(), @@ -1388,7 +1388,7 @@ impl<'db> LoopHeaderDefinitionKind<'db> { } #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update, get_size2::GetSize)] -pub(crate) struct DefinitionNodeKey(NodeKey); +pub struct DefinitionNodeKey(NodeKey); impl From<&ast::Alias> for DefinitionNodeKey { fn from(node: &ast::Alias) -> Self { diff --git a/crates/ty_python_semantic/src/semantic_index/expression.rs b/crates/ty_python_semantic/src/semantic_index/expression.rs index 3f6f159d179f9..b53fd632a5791 100644 --- a/crates/ty_python_semantic/src/semantic_index/expression.rs +++ b/crates/ty_python_semantic/src/semantic_index/expression.rs @@ -11,7 +11,7 @@ use salsa; /// `` is inferred as a type expression, while `` is inferred /// as a normal expression. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] -pub(crate) enum ExpressionKind { +pub enum ExpressionKind { Normal, TypeExpression, } @@ -32,18 +32,18 @@ pub(crate) enum ExpressionKind { /// * a field of a type that is a return type of a cross-module query /// * an argument of a cross-module query #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] -pub(crate) struct Expression<'db> { +pub struct Expression<'db> { /// The file in which the expression occurs. - pub(crate) file: File, + pub file: File, /// The scope in which the expression occurs. - pub(crate) file_scope: FileScopeId, + pub file_scope: FileScopeId, /// The expression node. #[no_eq] #[tracked] #[returns(ref)] - pub(crate) _node_ref: AstNodeRef, + pub _node_ref: AstNodeRef, /// An assignment statement, if this expression is immediately used as the rhs of that /// assignment. @@ -54,25 +54,21 @@ pub(crate) struct Expression<'db> { /// to the target, and so have `None` for this field.) #[no_eq] #[tracked] - pub(crate) assigned_to: Option>, + pub assigned_to: Option>, /// Should this expression be inferred as a normal expression or a type expression? - pub(crate) kind: ExpressionKind, + pub kind: ExpressionKind, } // The Salsa heap is tracked separately. impl get_size2::GetSize for Expression<'_> {} impl<'db> Expression<'db> { - pub(crate) fn node_ref<'ast>( - self, - db: &'db dyn Db, - parsed: &'ast ParsedModuleRef, - ) -> &'ast ast::Expr { + pub fn node_ref<'ast>(self, db: &'db dyn Db, parsed: &'ast ParsedModuleRef) -> &'ast ast::Expr { self._node_ref(db).node(parsed) } - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { self.file_scope(db).to_scope_id(db, self.file(db)) } } diff --git a/crates/ty_python_semantic/src/semantic_index/member.rs b/crates/ty_python_semantic/src/semantic_index/member.rs index 05c4f13d67642..10b7a6d80858b 100644 --- a/crates/ty_python_semantic/src/semantic_index/member.rs +++ b/crates/ty_python_semantic/src/semantic_index/member.rs @@ -13,13 +13,13 @@ use std::ops::{Deref, DerefMut}; /// A member access, e.g. `x.y` or `x[1]` or `x["foo"]`. #[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] -pub(crate) struct Member { +pub struct Member { expression: MemberExpr, flags: MemberFlags, } impl Member { - pub(crate) fn new(expression: MemberExpr) -> Self { + pub fn new(expression: MemberExpr) -> Self { Self { expression, flags: MemberFlags::empty(), @@ -29,38 +29,38 @@ impl Member { /// Returns the left most part of the member expression, e.g. `x` in `x.y.z`. /// /// This is the symbol on which the member access is performed. - pub(crate) fn symbol_name(&self) -> &str { + pub fn symbol_name(&self) -> &str { self.expression.symbol_name() } - pub(crate) fn expression(&self) -> &MemberExpr { + pub fn expression(&self) -> &MemberExpr { &self.expression } /// Is the place given a value in its containing scope? - pub(crate) const fn is_bound(&self) -> bool { + pub const fn is_bound(&self) -> bool { self.flags.contains(MemberFlags::IS_BOUND) } /// Is the place declared in its containing scope? - pub(crate) fn is_declared(&self) -> bool { + pub fn is_declared(&self) -> bool { self.flags.contains(MemberFlags::IS_DECLARED) } - pub(super) fn mark_bound(&mut self) { + pub fn mark_bound(&mut self) { self.insert_flags(MemberFlags::IS_BOUND); } - pub(super) fn mark_declared(&mut self) { + pub fn mark_declared(&mut self) { self.insert_flags(MemberFlags::IS_DECLARED); } - pub(super) fn mark_instance_attribute(&mut self) { + pub fn mark_instance_attribute(&mut self) { self.flags.insert(MemberFlags::IS_INSTANCE_ATTRIBUTE); } /// Is the place an instance attribute? - pub(crate) fn is_instance_attribute(&self) -> bool { + pub fn is_instance_attribute(&self) -> bool { let is_instance_attribute = self.flags.contains(MemberFlags::IS_INSTANCE_ATTRIBUTE); if is_instance_attribute { debug_assert!(self.is_instance_attribute_candidate()); @@ -82,7 +82,7 @@ impl Member { /// a method context, or whether the `` actually refers to the first /// parameter of the method (i.e. `self`). To answer those questions, /// use [`Self::as_instance_attribute`]. - pub(super) fn as_instance_attribute_candidate(&self) -> Option<&str> { + pub fn as_instance_attribute_candidate(&self) -> Option<&str> { let mut segments = self.expression().segments(); let first_segment = segments.next()?; @@ -102,17 +102,17 @@ impl Member { /// a method context, or whether the `` actually refers to the first /// parameter of the method (i.e. `self`). To answer those questions, /// use [`Self::is_instance_attribute`]. - pub(super) fn is_instance_attribute_candidate(&self) -> bool { + pub fn is_instance_attribute_candidate(&self) -> bool { self.as_instance_attribute_candidate().is_some() } /// Does the place expression have the form `self.{name}` (`self` is the first parameter of the method)? - pub(super) fn is_instance_attribute_named(&self, name: &str) -> bool { + pub fn is_instance_attribute_named(&self, name: &str) -> bool { self.as_instance_attribute() == Some(name) } /// Return `Some()` if the place expression is an instance attribute. - pub(crate) fn as_instance_attribute(&self) -> Option<&str> { + pub fn as_instance_attribute(&self) -> Option<&str> { if self.is_instance_attribute() { debug_assert!(self.as_instance_attribute_candidate().is_some()); self.as_instance_attribute_candidate() @@ -157,7 +157,7 @@ impl get_size2::GetSize for MemberFlags {} /// /// The symbol name can be extracted from the path by taking the text up to the first segment's start offset. #[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] -pub(crate) struct MemberExpr { +pub struct MemberExpr { /// The entire path as a single Name path: Name, /// Metadata for each segment (in forward order) @@ -166,11 +166,11 @@ pub(crate) struct MemberExpr { impl MemberExpr { #[cfg(test)] - pub(super) fn try_from_expr(expression: ast::ExprRef<'_>) -> Option { + pub fn try_from_expr(expression: ast::ExprRef<'_>) -> Option { MemberExprBuilder::visit_expr(expression).and_then(Self::try_from_builder) } - pub(super) fn try_from_builder(builder: MemberExprBuilder) -> Option { + pub fn try_from_builder(builder: MemberExprBuilder) -> Option { if builder.segments.is_empty() { None } else { @@ -196,15 +196,15 @@ impl MemberExpr { /// Returns the left most part of the member expression, e.g. `x` in `x.y.z`. /// /// This is the symbol on which the member access is performed. - pub(crate) fn symbol_name(&self) -> &str { + pub fn symbol_name(&self) -> &str { self.as_ref().symbol_name() } - pub(super) fn num_segments(&self) -> usize { + pub fn num_segments(&self) -> usize { self.segments.len() } - pub(crate) fn as_ref(&self) -> MemberExprRef<'_> { + pub fn as_ref(&self) -> MemberExprRef<'_> { MemberExprRef { path: self.path.as_str(), segments: SegmentsRef::from(&self.segments), @@ -214,13 +214,13 @@ impl MemberExpr { /// A builder for a [`MemberExpr`]. #[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] -pub(super) struct MemberExprBuilder { +pub struct MemberExprBuilder { path: Name, segments: SmallVec<[SegmentInfo; 8]>, } impl MemberExprBuilder { - pub(super) fn visit_expr(expr: ast::ExprRef) -> Option { + pub fn visit_expr(expr: ast::ExprRef) -> Option { match expr { ast::ExprRef::Name(name) => Some(MemberExprBuilder { path: name.id.clone(), @@ -248,7 +248,7 @@ impl MemberExprBuilder { } } - pub(super) fn visit_subscript_expr( + pub fn visit_subscript_expr( subscript_value: MemberExprBuilder, subscript_slice: &ast::Expr, ) -> Option { @@ -365,13 +365,13 @@ impl PartialEq<&MemberExpr> for MemberExprRef<'_> { /// Reference to a member expression. #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct MemberExprRef<'a> { +pub struct MemberExprRef<'a> { path: &'a str, segments: SegmentsRef<'a>, } impl<'a> MemberExprRef<'a> { - pub(super) fn symbol_name(&self) -> &'a str { + pub fn symbol_name(&self) -> &'a str { let end = self .segments .iter() @@ -389,7 +389,7 @@ impl<'a> MemberExprRef<'a> { SegmentsIterator::new(self.path, self.segments.iter()) } - pub(super) fn parent(&self) -> Option> { + pub fn parent(&self) -> Option> { let parent_segments = self.segments.parent()?; // The removed segment is always the last one. Find its start offset. @@ -424,7 +424,7 @@ pub struct ScopedMemberId; /// The members of a scope. Allows lookup by member path and [`ScopedMemberId`]. #[derive(Default, get_size2::GetSize)] -pub(super) struct MemberTable { +pub struct MemberTable { members: IndexVec, /// Map from member path to its ID. @@ -439,7 +439,7 @@ impl MemberTable { /// ## Panics /// If the ID is not valid for this table. #[track_caller] - pub(crate) fn member(&self, id: ScopedMemberId) -> &Member { + pub fn member(&self, id: ScopedMemberId) -> &Member { &self.members[id] } @@ -448,12 +448,12 @@ impl MemberTable { /// ## Panics /// If the ID is not valid for this table. #[track_caller] - pub(super) fn member_mut(&mut self, id: ScopedMemberId) -> &mut Member { + pub fn member_mut(&mut self, id: ScopedMemberId) -> &mut Member { &mut self.members[id] } /// Returns an iterator over all members in the table. - pub(crate) fn iter(&self) -> std::slice::Iter<'_, Member> { + pub fn iter(&self) -> std::slice::Iter<'_, Member> { self.members.iter() } @@ -462,10 +462,7 @@ impl MemberTable { } /// Returns the ID of the member with the given expression, if it exists. - pub(crate) fn member_id<'a>( - &self, - member: impl Into>, - ) -> Option { + pub fn member_id<'a>(&self, member: impl Into>) -> Option { let member = member.into(); let hash = Self::hash_member_expression_ref(&member); self.map @@ -473,7 +470,7 @@ impl MemberTable { .copied() } - pub(crate) fn place_id_by_instance_attribute_name(&self, name: &str) -> Option { + pub fn place_id_by_instance_attribute_name(&self, name: &str) -> Option { for (id, member) in self.members.iter_enumerated() { if member.is_instance_attribute_named(name) { return Some(id); @@ -500,7 +497,7 @@ impl std::fmt::Debug for MemberTable { } #[derive(Debug, Default)] -pub(super) struct MemberTableBuilder { +pub struct MemberTableBuilder { table: MemberTable, } @@ -508,7 +505,7 @@ impl MemberTableBuilder { /// Adds a member to the table or updates the flags of an existing member if it already exists. /// /// Members are identified by their expression, which is hashed to find the entry in the table. - pub(super) fn add(&mut self, mut member: Member) -> (ScopedMemberId, bool) { + pub fn add(&mut self, mut member: Member) -> (ScopedMemberId, bool) { let member_ref = member.expression.as_ref(); let hash = MemberTable::hash_member_expression_ref(&member_ref); let entry = self.table.map.entry( @@ -540,7 +537,7 @@ impl MemberTableBuilder { } } - pub(super) fn build(self) -> MemberTable { + pub fn build(self) -> MemberTable { let mut table = self.table; table.members.shrink_to_fit(); table.map.shrink_to_fit(|id| { diff --git a/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs index 8a5aa2ee61a5d..5ca72b12e1b33 100644 --- a/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs @@ -20,10 +20,10 @@ use crate::semantic_index::scope::FileScopeId; /// /// This is a TDD node ID in the shared reachability constraints graph. /// `ALWAYS_TRUE` means "no narrowing constraint" (the base type is unchanged). -pub(crate) type ScopedNarrowingConstraint = ScopedReachabilityConstraintId; +pub type ScopedNarrowingConstraint = ScopedReachabilityConstraintId; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum ConstraintKey { +pub enum ConstraintKey { NarrowingConstraint(ScopedNarrowingConstraint), NestedScope(FileScopeId), UseId(ScopedUseId), diff --git a/crates/ty_python_semantic/src/semantic_index/place.rs b/crates/ty_python_semantic/src/semantic_index/place.rs index 95bfe7f0de177..c90c106c511b3 100644 --- a/crates/ty_python_semantic/src/semantic_index/place.rs +++ b/crates/ty_python_semantic/src/semantic_index/place.rs @@ -12,7 +12,7 @@ use std::iter::FusedIterator; /// An expression that can be the target of a `Definition`. #[derive(Eq, PartialEq, Debug, get_size2::GetSize)] -pub(crate) enum PlaceExpr { +pub enum PlaceExpr { /// A simple symbol, e.g. `x`. Symbol(Symbol), @@ -24,7 +24,7 @@ impl PlaceExpr { /// Create a new `PlaceExpr` from a name. /// /// This always returns a `PlaceExpr::Symbol` with empty flags and `name`. - pub(crate) fn from_expr_name(name: &ast::ExprName) -> Self { + pub fn from_expr_name(name: &ast::ExprName) -> Self { PlaceExpr::Symbol(Symbol::new(name.id.clone())) } @@ -36,7 +36,7 @@ impl PlaceExpr { /// * name: `x` /// * attribute: `x.y` /// * subscripts with integer or string literals: `x[0]`, `x['key']` - pub(crate) fn try_from_expr<'e>(expr: impl Into>) -> Option { + pub fn try_from_expr<'e>(expr: impl Into>) -> Option { let expr = expr.into(); // For named expressions (walrus operator), extract the target. @@ -55,7 +55,7 @@ impl PlaceExpr { /// Tries to create a `PlaceExpr` from a member expression. /// /// Returns `None` if the expression is not a valid place expression and `Some` otherwise. - pub(super) fn try_from_member_expr(builder: MemberExprBuilder) -> Option { + pub fn try_from_member_expr(builder: MemberExprBuilder) -> Option { let member_expression = MemberExpr::try_from_builder(builder)?; Some(Self::Member(Member::new(member_expression))) } @@ -74,14 +74,14 @@ impl std::fmt::Display for PlaceExpr { /// /// Needed so that we can iterate over all places without cloning them. #[derive(Eq, PartialEq, Debug, Copy, Clone)] -pub(crate) enum PlaceExprRef<'a> { +pub enum PlaceExprRef<'a> { Symbol(&'a Symbol), Member(&'a Member), } impl<'a> PlaceExprRef<'a> { /// Returns `Some` if the reference is a `Symbol`, otherwise `None`. - pub(crate) const fn as_symbol(self) -> Option<&'a Symbol> { + pub const fn as_symbol(self) -> Option<&'a Symbol> { if let PlaceExprRef::Symbol(symbol) = self { Some(symbol) } else { @@ -90,25 +90,25 @@ impl<'a> PlaceExprRef<'a> { } /// Returns `true` if the reference is a `Symbol`, otherwise `false`. - pub(crate) const fn is_symbol(self) -> bool { + pub const fn is_symbol(self) -> bool { matches!(self, PlaceExprRef::Symbol(_)) } - pub(crate) fn is_declared(self) -> bool { + pub fn is_declared(self) -> bool { match self { Self::Symbol(symbol) => symbol.is_declared(), Self::Member(member) => member.is_declared(), } } - pub(crate) const fn is_bound(self) -> bool { + pub const fn is_bound(self) -> bool { match self { PlaceExprRef::Symbol(symbol) => symbol.is_bound(), PlaceExprRef::Member(member) => member.is_bound(), } } - pub(crate) fn num_member_segments(self) -> usize { + pub fn num_member_segments(self) -> usize { match self { PlaceExprRef::Symbol(_) => 0, PlaceExprRef::Member(member) => member.expression().num_segments(), @@ -154,7 +154,7 @@ pub enum ScopedPlaceId { } #[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)] -pub(crate) struct PlaceTable { +pub struct PlaceTable { symbols: SymbolTable, members: MemberTable, } @@ -163,10 +163,7 @@ impl PlaceTable { /// Iterate over the "root" expressions of the place (e.g. `x.y.z`, `x.y`, `x` for `x.y.z[0]`). /// /// Note, this iterator may skip some parents if they are not defined in the current scope. - pub(crate) fn parents<'a>( - &'a self, - place_expr: impl Into>, - ) -> ParentPlaceIter<'a> { + pub fn parents<'a>(&'a self, place_expr: impl Into>) -> ParentPlaceIter<'a> { match place_expr.into() { PlaceExprRef::Symbol(_) => ParentPlaceIter::for_symbol(), PlaceExprRef::Member(member) => { @@ -176,12 +173,12 @@ impl PlaceTable { } /// Iterator over all symbols in this scope. - pub(crate) fn symbols(&self) -> std::slice::Iter<'_, Symbol> { + pub fn symbols(&self) -> std::slice::Iter<'_, Symbol> { self.symbols.iter() } /// Iterator over all members in this scope. - pub(crate) fn members(&self) -> std::slice::Iter<'_, Member> { + pub fn members(&self) -> std::slice::Iter<'_, Member> { self.members.iter() } @@ -190,14 +187,14 @@ impl PlaceTable { /// ## Panics /// If the symbol ID is not found in the table. #[track_caller] - pub(crate) fn symbol(&self, id: ScopedSymbolId) -> &Symbol { + pub fn symbol(&self, id: ScopedSymbolId) -> &Symbol { self.symbols.symbol(id) } /// Looks up a symbol by its name and returns a reference to it, if it exists. /// /// This should only be used in diagnostics and tests. - pub(crate) fn symbol_by_name(&self, name: &str) -> Option<&Symbol> { + pub fn symbol_by_name(&self, name: &str) -> Option<&Symbol> { self.symbols.symbol_id(name).map(|id| self.symbol(id)) } @@ -206,20 +203,17 @@ impl PlaceTable { /// ## Panics /// If the member ID is not found in the table. #[track_caller] - pub(crate) fn member(&self, id: ScopedMemberId) -> &Member { + pub fn member(&self, id: ScopedMemberId) -> &Member { self.members.member(id) } /// Returns the [`ScopedSymbolId`] of the place named `name`. - pub(crate) fn symbol_id(&self, name: &str) -> Option { + pub fn symbol_id(&self, name: &str) -> Option { self.symbols.symbol_id(name) } /// Returns the [`ScopedPlaceId`] of the place expression. - pub(crate) fn place_id<'e>( - &self, - place_expr: impl Into>, - ) -> Option { + pub fn place_id<'e>(&self, place_expr: impl Into>) -> Option { let place_expr = place_expr.into(); match place_expr { @@ -235,23 +229,20 @@ impl PlaceTable { /// ## Panics /// If the place ID is not found in the table. #[track_caller] - pub(crate) fn place(&self, place_id: impl Into) -> PlaceExprRef<'_> { + pub fn place(&self, place_id: impl Into) -> PlaceExprRef<'_> { match place_id.into() { ScopedPlaceId::Symbol(symbol) => self.symbol(symbol).into(), ScopedPlaceId::Member(member) => self.member(member).into(), } } - pub(crate) fn member_id_by_instance_attribute_name( - &self, - name: &str, - ) -> Option { + pub fn member_id_by_instance_attribute_name(&self, name: &str) -> Option { self.members.place_id_by_instance_attribute_name(name) } } #[derive(Default)] -pub(crate) struct PlaceTableBuilder { +pub struct PlaceTableBuilder { symbols: SymbolTableBuilder, member: MemberTableBuilder, @@ -261,7 +252,7 @@ pub(crate) struct PlaceTableBuilder { impl PlaceTableBuilder { /// Looks up a place ID by its expression. - pub(crate) fn place_id(&self, expression: PlaceExprRef) -> Option { + pub fn place_id(&self, expression: PlaceExprRef) -> Option { match expression { PlaceExprRef::Symbol(symbol) => self.symbols.symbol_id(symbol.name()).map(Into::into), PlaceExprRef::Member(member) => { @@ -271,51 +262,51 @@ impl PlaceTableBuilder { } #[track_caller] - pub(super) fn symbol(&self, id: ScopedSymbolId) -> &Symbol { + pub fn symbol(&self, id: ScopedSymbolId) -> &Symbol { self.symbols.symbol(id) } - pub(super) fn symbol_id(&self, name: &str) -> Option { + pub fn symbol_id(&self, name: &str) -> Option { self.symbols.symbol_id(name) } #[track_caller] - pub(super) fn symbol_mut(&mut self, id: ScopedSymbolId) -> &mut Symbol { + pub fn symbol_mut(&mut self, id: ScopedSymbolId) -> &mut Symbol { self.symbols.symbol_mut(id) } #[track_caller] - pub(super) fn member_mut(&mut self, id: ScopedMemberId) -> &mut Member { + pub fn member_mut(&mut self, id: ScopedMemberId) -> &mut Member { self.member.member_mut(id) } #[track_caller] - pub(crate) fn place(&self, place_id: impl Into) -> PlaceExprRef<'_> { + pub fn place(&self, place_id: impl Into) -> PlaceExprRef<'_> { match place_id.into() { ScopedPlaceId::Symbol(id) => PlaceExprRef::Symbol(self.symbols.symbol(id)), ScopedPlaceId::Member(id) => PlaceExprRef::Member(self.member.member(id)), } } - pub(crate) fn associated_place_ids(&self, place: ScopedPlaceId) -> &[ScopedMemberId] { + pub fn associated_place_ids(&self, place: ScopedPlaceId) -> &[ScopedMemberId] { match place { ScopedPlaceId::Symbol(symbol) => &self.associated_symbol_members[symbol], ScopedPlaceId::Member(member) => &self.associated_sub_members[member], } } - pub(crate) fn iter(&self) -> impl Iterator> { + pub fn iter(&self) -> impl Iterator> { self.symbols .iter() .map(Into::into) .chain(self.member.iter().map(PlaceExprRef::Member)) } - pub(crate) fn symbols(&self) -> impl Iterator { + pub fn symbols(&self) -> impl Iterator { self.symbols.iter() } - pub(crate) fn add_symbol(&mut self, symbol: Symbol) -> (ScopedSymbolId, bool) { + pub fn add_symbol(&mut self, symbol: Symbol) -> (ScopedSymbolId, bool) { let (id, is_new) = self.symbols.add(symbol); if is_new { @@ -326,7 +317,7 @@ impl PlaceTableBuilder { (id, is_new) } - pub(crate) fn add_member(&mut self, member: Member) -> (ScopedMemberId, bool) { + pub fn add_member(&mut self, member: Member) -> (ScopedMemberId, bool) { let (id, is_new) = self.member.add(member); if is_new { @@ -353,7 +344,7 @@ impl PlaceTableBuilder { (id, is_new) } - pub(crate) fn add_place(&mut self, place: PlaceExpr) -> (ScopedPlaceId, bool) { + pub fn add_place(&mut self, place: PlaceExpr) -> (ScopedPlaceId, bool) { match place { PlaceExpr::Symbol(symbol) => { let (id, is_new) = self.add_symbol(symbol); @@ -367,7 +358,7 @@ impl PlaceTableBuilder { } #[track_caller] - pub(super) fn mark_bound(&mut self, id: ScopedPlaceId) { + pub fn mark_bound(&mut self, id: ScopedPlaceId) { match id { ScopedPlaceId::Symbol(symbol_id) => { self.symbol_mut(symbol_id).mark_bound(); @@ -379,7 +370,7 @@ impl PlaceTableBuilder { } #[track_caller] - pub(super) fn mark_declared(&mut self, id: ScopedPlaceId) { + pub fn mark_declared(&mut self, id: ScopedPlaceId) { match id { ScopedPlaceId::Symbol(symbol_id) => { self.symbol_mut(symbol_id).mark_declared(); @@ -390,7 +381,7 @@ impl PlaceTableBuilder { } } - pub(crate) fn finish(self) -> PlaceTable { + pub fn finish(self) -> PlaceTable { PlaceTable { symbols: self.symbols.build(), members: self.member.build(), @@ -468,7 +459,7 @@ impl FilePlaceId { self.scope } - pub(crate) fn scoped_place_id(self) -> ScopedPlaceId { + pub fn scoped_place_id(self) -> ScopedPlaceId { self.scoped_place_id } } @@ -479,7 +470,7 @@ impl From for ScopedPlaceId { } } -pub(crate) struct ParentPlaceIter<'a> { +pub struct ParentPlaceIter<'a> { state: Option>, } @@ -516,11 +507,11 @@ impl<'a> ParentPlaceIterState<'a> { } impl<'a> ParentPlaceIter<'a> { - pub(super) fn for_symbol() -> Self { + pub fn for_symbol() -> Self { ParentPlaceIter { state: None } } - pub(super) fn for_member( + pub fn for_member( expression: &'a MemberExpr, symbol_table: &'a SymbolTable, member_table: &'a MemberTable, diff --git a/crates/ty_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs index 39a2b2fbeaa9c..54e7c5d09dc32 100644 --- a/crates/ty_python_semantic/src/semantic_index/predicate.rs +++ b/crates/ty_python_semantic/src/semantic_index/predicate.rs @@ -19,14 +19,14 @@ use crate::semantic_index::symbol::ScopedSymbolId; // A scoped identifier for each `Predicate` in a scope. #[derive(Clone, Debug, Copy, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)] -pub(crate) struct ScopedPredicateId(u32); +pub struct ScopedPredicateId(u32); impl ScopedPredicateId { /// A special ID that is used for an "always true" predicate. - pub(crate) const ALWAYS_TRUE: ScopedPredicateId = ScopedPredicateId(0xffff_ffff); + pub const ALWAYS_TRUE: ScopedPredicateId = ScopedPredicateId(0xffff_ffff); /// A special ID that is used for an "always false" predicate. - pub(crate) const ALWAYS_FALSE: ScopedPredicateId = ScopedPredicateId(0xffff_fffe); + pub const ALWAYS_FALSE: ScopedPredicateId = ScopedPredicateId(0xffff_fffe); const SMALLEST_TERMINAL: ScopedPredicateId = Self::ALWAYS_FALSE; @@ -51,10 +51,10 @@ impl Idx for ScopedPredicateId { } // A collection of predicates for a given scope. -pub(crate) type Predicates<'db> = IndexVec>; +pub type Predicates<'db> = IndexVec>; #[derive(Debug, Default)] -pub(crate) struct PredicatesBuilder<'db> { +pub struct PredicatesBuilder<'db> { predicates: IndexVec>, } @@ -62,30 +62,30 @@ impl<'db> PredicatesBuilder<'db> { /// Adds a predicate. Note that we do not deduplicate predicates. If you add a `Predicate` /// more than once, you will get distinct `ScopedPredicateId`s for each one. (This lets you /// model predicates that might evaluate to different values at different points of execution.) - pub(crate) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { + pub fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { self.predicates.push(predicate) } - pub(crate) fn build(mut self) -> Predicates<'db> { + pub fn build(mut self) -> Predicates<'db> { self.predicates.shrink_to_fit(); self.predicates } } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct Predicate<'db> { - pub(crate) node: PredicateNode<'db>, - pub(crate) is_positive: bool, +pub struct Predicate<'db> { + pub node: PredicateNode<'db>, + pub is_positive: bool, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) enum PredicateOrLiteral<'db> { +pub enum PredicateOrLiteral<'db> { Literal(bool), Predicate(Predicate<'db>), } impl PredicateOrLiteral<'_> { - pub(crate) fn negated(self) -> Self { + pub fn negated(self) -> Self { match self { PredicateOrLiteral::Literal(value) => PredicateOrLiteral::Literal(!value), PredicateOrLiteral::Predicate(Predicate { node, is_positive }) => { @@ -99,17 +99,17 @@ impl PredicateOrLiteral<'_> { } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct CallableAndCallExpr<'db> { - pub(crate) callable: Expression<'db>, - pub(crate) call_expr: Expression<'db>, +pub struct CallableAndCallExpr<'db> { + pub callable: Expression<'db>, + pub call_expr: Expression<'db>, /// Whether the call is wrapped in an `await` expression. If `true`, `call_expr` refers to the /// `await` expression rather than the call itself. This is used to detect terminal `await`s of /// async functions that return `Never`. - pub(crate) is_await: bool, + pub is_await: bool, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) enum PredicateNode<'db> { +pub enum PredicateNode<'db> { Expression(Expression<'db>), ReturnsNever(CallableAndCallExpr<'db>), Pattern(PatternPredicate<'db>), @@ -117,20 +117,20 @@ pub(crate) enum PredicateNode<'db> { } #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) enum ClassPatternKind { +pub enum ClassPatternKind { Irrefutable, Refutable, } impl ClassPatternKind { - pub(crate) fn is_irrefutable(self) -> bool { + pub fn is_irrefutable(self) -> bool { matches!(self, ClassPatternKind::Irrefutable) } } /// Pattern kinds for which we support type narrowing and/or static reachability analysis. #[derive(Debug, Clone, Hash, PartialEq, salsa::Update, get_size2::GetSize)] -pub(crate) enum PatternPredicateKind<'db> { +pub enum PatternPredicateKind<'db> { Singleton(Singleton), Value(Expression<'db>), Or(Vec>), @@ -141,27 +141,27 @@ pub(crate) enum PatternPredicateKind<'db> { } #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] -pub(crate) struct PatternPredicate<'db> { - pub(crate) file: File, +pub struct PatternPredicate<'db> { + pub file: File, - pub(crate) file_scope: FileScopeId, + pub file_scope: FileScopeId, - pub(crate) subject: Expression<'db>, + pub subject: Expression<'db>, #[returns(ref)] - pub(crate) kind: PatternPredicateKind<'db>, + pub kind: PatternPredicateKind<'db>, - pub(crate) guard: Option>, + pub guard: Option>, /// A reference to the pattern of the previous match case - pub(crate) previous_predicate: Option>>, + pub previous_predicate: Option>>, } // The Salsa heap is tracked separately. impl get_size2::GetSize for PatternPredicate<'_> {} impl<'db> PatternPredicate<'db> { - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { self.file_scope(db).to_scope_id(db, self.file(db)) } } @@ -207,8 +207,8 @@ impl<'db> PatternPredicate<'db> { /// /// [Truthiness]: [crate::types::Truthiness] #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] -pub(crate) struct StarImportPlaceholderPredicate<'db> { - pub(crate) importing_file: File, +pub struct StarImportPlaceholderPredicate<'db> { + pub importing_file: File, /// Each symbol imported by a `*` import has a separate predicate associated with it: /// this field identifies which symbol that is. @@ -219,16 +219,16 @@ pub(crate) struct StarImportPlaceholderPredicate<'db> { /// for valid `*`-import definitions, and valid `*`-import definitions can only ever /// exist in the global scope; thus, we know that the `symbol_id` here will be relative /// to the global scope of the importing file. - pub(crate) symbol_id: ScopedSymbolId, + pub symbol_id: ScopedSymbolId, - pub(crate) referenced_file: File, + pub referenced_file: File, } // The Salsa heap is tracked separately. impl get_size2::GetSize for StarImportPlaceholderPredicate<'_> {} impl<'db> StarImportPlaceholderPredicate<'db> { - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { // See doc-comment above [`StarImportPlaceholderPredicate::symbol_id`]: // valid `*`-import definitions can only take place in the global scope. global_scope(db, self.importing_file(db)) diff --git a/crates/ty_python_semantic/src/semantic_index/re_exports.rs b/crates/ty_python_semantic/src/semantic_index/re_exports.rs index 49fa339a96ec9..dec3473cf1e9d 100644 --- a/crates/ty_python_semantic/src/semantic_index/re_exports.rs +++ b/crates/ty_python_semantic/src/semantic_index/re_exports.rs @@ -36,7 +36,7 @@ use crate::Db; cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size) ] -pub(super) fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> { +pub fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> { let module = parsed_module(db, file).load(db); let mut finder = ExportFinder::new(db, file); finder.visit_body(module.suite()); diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index a678ec1930efc..fd9274eeeaa06 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -232,7 +232,7 @@ use crate::types::{ /// reachability constraints are normalized, so equivalent constraints are guaranteed to have equal /// IDs. #[derive(Clone, Copy, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] -pub(crate) struct ScopedReachabilityConstraintId(u32); +pub struct ScopedReachabilityConstraintId(u32); impl std::fmt::Debug for ScopedReachabilityConstraintId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -273,15 +273,15 @@ struct InteriorNode { impl ScopedReachabilityConstraintId { /// A special ID that is used for an "always true" / "always visible" constraint. - pub(crate) const ALWAYS_TRUE: ScopedReachabilityConstraintId = + pub const ALWAYS_TRUE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId(0xffff_ffff); /// A special ID that is used for an ambiguous constraint. - pub(crate) const AMBIGUOUS: ScopedReachabilityConstraintId = + pub const AMBIGUOUS: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId(0xffff_fffe); /// A special ID that is used for an "always false" / "never visible" constraint. - pub(crate) const ALWAYS_FALSE: ScopedReachabilityConstraintId = + pub const ALWAYS_FALSE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId(0xffff_fffd); fn is_terminal(self) -> bool { @@ -447,7 +447,7 @@ fn analyze_pattern_predicate<'db>(db: &'db dyn Db, predicate: PatternPredicate<' /// A collection of reachability constraints for a given scope. #[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct ReachabilityConstraints { +pub struct ReachabilityConstraints { /// The interior TDD nodes that were marked as used when being built. used_interiors: Box<[InteriorNode]>, /// A bit vector indicating which interior TDD nodes were marked as used. This is indexed by @@ -457,7 +457,7 @@ pub(crate) struct ReachabilityConstraints { } #[derive(Debug, Default, PartialEq, Eq)] -pub(crate) struct ReachabilityConstraintsBuilder { +pub struct ReachabilityConstraintsBuilder { interiors: IndexVec, interior_used: IndexVec, interior_cache: FxHashMap, @@ -479,7 +479,7 @@ pub(crate) struct ReachabilityConstraintsBuilder { } impl ReachabilityConstraintsBuilder { - pub(crate) fn build(self) -> ReachabilityConstraints { + pub fn build(self) -> ReachabilityConstraints { let used_indices = RankBitBox::from_bits(self.interior_used.iter().copied()); let used_interiors = (self.interiors.into_iter()) .zip(self.interior_used) @@ -494,7 +494,7 @@ impl ReachabilityConstraintsBuilder { /// Marks that a particular TDD node is used. This lets us throw away interior nodes that were /// only calculated for intermediate values, and which don't need to be included in the final /// built result. - pub(crate) fn mark_used(&mut self, node: ScopedReachabilityConstraintId) { + pub fn mark_used(&mut self, node: ScopedReachabilityConstraintId) { if !node.is_terminal() && !self.interior_used[node] { self.interior_used[node] = true; let node = self.interiors[node]; @@ -571,10 +571,7 @@ impl ReachabilityConstraintsBuilder { /// advantage of the fact that the [`Predicates`] arena does not deduplicate `Predicate`s. /// You can add a `Predicate` multiple times, yielding different `ScopedPredicateId`s, which /// you can then create separate TDD atoms for. - pub(crate) fn add_atom( - &mut self, - predicate: ScopedPredicateId, - ) -> ScopedReachabilityConstraintId { + pub fn add_atom(&mut self, predicate: ScopedPredicateId) -> ScopedReachabilityConstraintId { if predicate == ScopedPredicateId::ALWAYS_FALSE { ScopedReachabilityConstraintId::ALWAYS_FALSE } else if predicate == ScopedPredicateId::ALWAYS_TRUE { @@ -590,7 +587,7 @@ impl ReachabilityConstraintsBuilder { } /// Adds a new reachability constraint that is the ternary NOT of an existing one. - pub(crate) fn add_not_constraint( + pub fn add_not_constraint( &mut self, a: ScopedReachabilityConstraintId, ) -> ScopedReachabilityConstraintId { @@ -625,7 +622,7 @@ impl ReachabilityConstraintsBuilder { } /// Adds a new reachability constraint that is the ternary OR of two existing ones. - pub(crate) fn add_or_constraint( + pub fn add_or_constraint( &mut self, a: ScopedReachabilityConstraintId, b: ScopedReachabilityConstraintId, @@ -695,7 +692,7 @@ impl ReachabilityConstraintsBuilder { } /// Adds a new reachability constraint that is the ternary AND of two existing ones. - pub(crate) fn add_and_constraint( + pub fn add_and_constraint( &mut self, a: ScopedReachabilityConstraintId, b: ScopedReachabilityConstraintId, @@ -812,7 +809,7 @@ impl ReachabilityConstraints { /// - `ALWAYS_FALSE`: this path is impossible → Never /// /// The final result is the union of all path results. - pub(crate) fn narrow_by_constraint<'db>( + pub fn narrow_by_constraint<'db>( &self, db: &'db dyn Db, predicates: &Predicates<'db>, @@ -944,7 +941,7 @@ impl ReachabilityConstraints { } /// Analyze the statically known reachability for a given constraint. - pub(crate) fn evaluate<'db>( + pub fn evaluate<'db>( &self, db: &'db dyn Db, predicates: &Predicates<'db>, diff --git a/crates/ty_python_semantic/src/semantic_index/scope.rs b/crates/ty_python_semantic/src/semantic_index/scope.rs index df10f6e7dd660..bf4482e84b138 100644 --- a/crates/ty_python_semantic/src/semantic_index/scope.rs +++ b/crates/ty_python_semantic/src/semantic_index/scope.rs @@ -26,16 +26,16 @@ pub struct ScopeId<'db> { impl get_size2::GetSize for ScopeId<'_> {} impl<'db> ScopeId<'db> { - pub(crate) fn is_annotation(self, db: &'db dyn Db) -> bool { + pub fn is_annotation(self, db: &'db dyn Db) -> bool { self.node(db).scope_kind().is_annotation() } - pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind { + pub fn node(self, db: &dyn Db) -> &NodeWithScopeKind { self.scope(db).node() } /// Returns `true` if this scope may require type context from its parent scope. - pub(crate) fn accepts_type_context(self, db: &dyn Db) -> bool { + pub fn accepts_type_context(self, db: &dyn Db) -> bool { matches!( self.node(db), NodeWithScopeKind::ListComprehension(_) @@ -44,12 +44,12 @@ impl<'db> ScopeId<'db> { ) } - pub(crate) fn scope(self, db: &dyn Db) -> &Scope { + pub fn scope(self, db: &dyn Db) -> &Scope { semantic_index(db, self.file(db)).scope(self.file_scope_id(db)) } #[cfg(test)] - pub(crate) fn name<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast str { + pub fn name<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast str { match self.node(db) { NodeWithScopeKind::Module => "", NodeWithScopeKind::Class(class) | NodeWithScopeKind::ClassTypeParameters(class) => { @@ -95,13 +95,13 @@ impl FileScopeId { index.scope_ids_by_scope[self] } - pub(crate) fn is_generator_function(self, index: &SemanticIndex) -> bool { + pub fn is_generator_function(self, index: &SemanticIndex) -> bool { index.generator_functions.contains(&self) } } #[derive(Debug, salsa::Update, get_size2::GetSize)] -pub(crate) struct Scope { +pub struct Scope { /// The parent scope, if any. parent: Option, @@ -119,7 +119,7 @@ pub(crate) struct Scope { } impl Scope { - pub(super) fn new( + pub fn new( parent: Option, node: NodeWithScopeKind, descendants: Range, @@ -135,45 +135,45 @@ impl Scope { } } - pub(crate) fn parent(&self) -> Option { + pub fn parent(&self) -> Option { self.parent } - pub(crate) fn node(&self) -> &NodeWithScopeKind { + pub fn node(&self) -> &NodeWithScopeKind { &self.node } - pub(crate) fn kind(&self) -> ScopeKind { + pub fn kind(&self) -> ScopeKind { self.node().scope_kind() } - pub(crate) fn visibility(&self) -> ScopeVisibility { + pub fn visibility(&self) -> ScopeVisibility { self.kind().visibility() } - pub(crate) fn descendants(&self) -> Range { + pub fn descendants(&self) -> Range { self.descendants.clone() } - pub(super) fn extend_descendants(&mut self, children_end: FileScopeId) { + pub fn extend_descendants(&mut self, children_end: FileScopeId) { self.descendants = self.descendants.start..children_end; } - pub(crate) fn is_eager(&self) -> bool { + pub fn is_eager(&self) -> bool { self.kind().is_eager() } - pub(crate) fn reachability(&self) -> ScopedReachabilityConstraintId { + pub fn reachability(&self) -> ScopedReachabilityConstraintId { self.reachability } - pub(crate) fn in_type_checking_block(&self) -> bool { + pub fn in_type_checking_block(&self) -> bool { self.in_type_checking_block } } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, get_size2::GetSize)] -pub(crate) enum ScopeVisibility { +pub enum ScopeVisibility { /// The scope is private (e.g. function, type alias, comprehension scope). Private, /// The scope is public (e.g. module, class scope). @@ -181,17 +181,17 @@ pub(crate) enum ScopeVisibility { } impl ScopeVisibility { - pub(crate) const fn is_public(self) -> bool { + pub const fn is_public(self) -> bool { matches!(self, ScopeVisibility::Public) } - pub(crate) const fn is_private(self) -> bool { + pub const fn is_private(self) -> bool { matches!(self, ScopeVisibility::Private) } } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, get_size2::GetSize)] -pub(crate) enum ScopeLaziness { +pub enum ScopeLaziness { /// The scope is evaluated lazily (e.g. function, type alias scope). Lazy, /// The scope is evaluated eagerly (e.g. module, class, comprehension scope). @@ -199,17 +199,17 @@ pub(crate) enum ScopeLaziness { } impl ScopeLaziness { - pub(crate) const fn is_eager(self) -> bool { + pub const fn is_eager(self) -> bool { matches!(self, ScopeLaziness::Eager) } - pub(crate) const fn is_lazy(self) -> bool { + pub const fn is_lazy(self) -> bool { matches!(self, ScopeLaziness::Lazy) } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(crate) enum ScopeKind { +pub enum ScopeKind { Module, TypeParams, Class, @@ -220,11 +220,11 @@ pub(crate) enum ScopeKind { } impl ScopeKind { - pub(crate) const fn is_eager(self) -> bool { + pub const fn is_eager(self) -> bool { self.laziness().is_eager() } - pub(crate) const fn laziness(self) -> ScopeLaziness { + pub const fn laziness(self) -> ScopeLaziness { match self { ScopeKind::Module | ScopeKind::Class @@ -234,7 +234,7 @@ impl ScopeKind { } } - pub(crate) const fn visibility(self) -> ScopeVisibility { + pub const fn visibility(self) -> ScopeVisibility { match self { ScopeKind::Module | ScopeKind::Class => ScopeVisibility::Public, ScopeKind::TypeParams @@ -245,7 +245,7 @@ impl ScopeKind { } } - pub(crate) const fn is_function_like(self) -> bool { + pub const fn is_function_like(self) -> bool { // Type parameter scopes behave like function scopes in terms of name resolution; CPython // symbol table also uses the term "function-like" for these scopes. matches!( @@ -258,26 +258,26 @@ impl ScopeKind { ) } - pub(crate) const fn is_class(self) -> bool { + pub const fn is_class(self) -> bool { matches!(self, ScopeKind::Class) } - pub(crate) const fn is_module(self) -> bool { + pub const fn is_module(self) -> bool { matches!(self, ScopeKind::Module) } - pub(crate) const fn is_annotation(self) -> bool { + pub const fn is_annotation(self) -> bool { matches!(self, ScopeKind::TypeParams | ScopeKind::TypeAlias) } - pub(crate) const fn is_non_lambda_function(self) -> bool { + pub const fn is_non_lambda_function(self) -> bool { matches!(self, ScopeKind::Function) } } /// Reference to a node that introduces a new scope. #[derive(Copy, Clone, Debug)] -pub(crate) enum NodeWithScopeRef<'a> { +pub enum NodeWithScopeRef<'a> { Module, Class(&'a ast::StmtClassDef), Function(&'a ast::StmtFunctionDef), @@ -296,7 +296,7 @@ impl NodeWithScopeRef<'_> { /// Converts the unowned reference to an owned [`NodeWithScopeKind`]. /// /// Note that node wrapped by `self` must be a child of `module`. - pub(super) fn to_kind(self, module: &ParsedModuleRef) -> NodeWithScopeKind { + pub fn to_kind(self, module: &ParsedModuleRef) -> NodeWithScopeKind { match self { NodeWithScopeRef::Module => NodeWithScopeKind::Module, NodeWithScopeRef::Class(class) => { @@ -335,7 +335,7 @@ impl NodeWithScopeRef<'_> { } } - pub(crate) fn node_key(self) -> NodeWithScopeKey { + pub fn node_key(self) -> NodeWithScopeKey { match self { NodeWithScopeRef::Module => NodeWithScopeKey::Module, NodeWithScopeRef::Class(class) => NodeWithScopeKey::Class(NodeKey::from_node(class)), @@ -375,7 +375,7 @@ impl NodeWithScopeRef<'_> { /// Node that introduces a new scope. #[derive(Clone, Debug, salsa::Update, get_size2::GetSize)] -pub(crate) enum NodeWithScopeKind { +pub enum NodeWithScopeKind { Module, Class(AstNodeRef), ClassTypeParameters(AstNodeRef), @@ -391,7 +391,7 @@ pub(crate) enum NodeWithScopeKind { } impl NodeWithScopeKind { - pub(crate) const fn scope_kind(&self) -> ScopeKind { + pub const fn scope_kind(&self) -> ScopeKind { match self { Self::Module => ScopeKind::Module, Self::Class(_) => ScopeKind::Class, @@ -408,40 +408,40 @@ impl NodeWithScopeKind { } } - pub(crate) fn as_class(&self) -> Option<&AstNodeRef> { + pub fn as_class(&self) -> Option<&AstNodeRef> { match self { Self::Class(class) => Some(class), _ => None, } } - pub(crate) fn expect_class(&self) -> &AstNodeRef { + pub fn expect_class(&self) -> &AstNodeRef { self.as_class().expect("expected class") } - pub(crate) fn as_function(&self) -> Option<&AstNodeRef> { + pub fn as_function(&self) -> Option<&AstNodeRef> { match self { Self::Function(function) => Some(function), _ => None, } } - pub(crate) fn expect_function(&self) -> &AstNodeRef { + pub fn expect_function(&self) -> &AstNodeRef { self.as_function().expect("expected function") } - pub(crate) fn as_type_alias(&self) -> Option<&AstNodeRef> { + pub fn as_type_alias(&self) -> Option<&AstNodeRef> { match self { Self::TypeAlias(type_alias) => Some(type_alias), _ => None, } } - pub(crate) fn expect_type_alias(&self) -> &AstNodeRef { + pub fn expect_type_alias(&self) -> &AstNodeRef { self.as_type_alias().expect("expected type alias") } - pub(crate) fn generic_context<'db>( + pub fn generic_context<'db>( &self, db: &'db dyn Db, index: &SemanticIndex<'db>, @@ -477,7 +477,7 @@ impl NodeWithScopeKind { /// /// This is used to compute relative node indices for expressions within the scope, /// providing a stable anchor that only changes when the scope-introducing node changes. - pub(crate) fn node_index(&self) -> Option { + pub fn node_index(&self) -> Option { match self { Self::Module => None, Self::Class(class) => Some(class.index()), @@ -496,7 +496,7 @@ impl NodeWithScopeKind { } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] -pub(crate) enum NodeWithScopeKey { +pub enum NodeWithScopeKey { Module, Class(NodeKey), ClassTypeParameters(NodeKey), diff --git a/crates/ty_python_semantic/src/semantic_index/symbol.rs b/crates/ty_python_semantic/src/semantic_index/symbol.rs index 8aea606f597bf..074528307a95f 100644 --- a/crates/ty_python_semantic/src/semantic_index/symbol.rs +++ b/crates/ty_python_semantic/src/semantic_index/symbol.rs @@ -13,7 +13,7 @@ pub struct ScopedSymbolId; /// A symbol in a given scope. #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)] -pub(crate) struct Symbol { +pub struct Symbol { name: Name, flags: SymbolFlags, } @@ -45,39 +45,39 @@ bitflags! { impl get_size2::GetSize for SymbolFlags {} impl Symbol { - pub(crate) const fn new(name: Name) -> Self { + pub const fn new(name: Name) -> Self { Self { name, flags: SymbolFlags::empty(), } } - pub(crate) fn name(&self) -> &Name { + pub fn name(&self) -> &Name { &self.name } /// Is the symbol used in its containing scope? - pub(crate) fn is_used(&self) -> bool { + pub fn is_used(&self) -> bool { self.flags.contains(SymbolFlags::IS_USED) } /// Is the symbol given a value in its containing scope? - pub(crate) const fn is_bound(&self) -> bool { + pub const fn is_bound(&self) -> bool { self.flags.contains(SymbolFlags::IS_BOUND) } /// Is the symbol declared in its containing scope? - pub(crate) fn is_declared(&self) -> bool { + pub fn is_declared(&self) -> bool { self.flags.contains(SymbolFlags::IS_DECLARED) } /// Is the symbol `global` its containing scope? - pub(crate) fn is_global(&self) -> bool { + pub fn is_global(&self) -> bool { self.flags.contains(SymbolFlags::MARKED_GLOBAL) } /// Is the symbol `nonlocal` its containing scope? - pub(crate) fn is_nonlocal(&self) -> bool { + pub fn is_nonlocal(&self) -> bool { self.flags.contains(SymbolFlags::MARKED_NONLOCAL) } @@ -109,27 +109,27 @@ impl Symbol { /// In cases like this, the resolution isn't known until runtime, and in fact it varies from /// one use to the next. The semantic index alone can't resolve this, and instead it's a /// special case in type inference (see `infer_place_load`). - pub(crate) fn is_local(&self) -> bool { + pub fn is_local(&self) -> bool { !self.is_global() && !self.is_nonlocal() && (self.is_bound() || self.is_declared()) } - pub(crate) const fn is_reassigned(&self) -> bool { + pub const fn is_reassigned(&self) -> bool { self.flags.contains(SymbolFlags::IS_REASSIGNED) } - pub(crate) fn is_parameter(&self) -> bool { + pub fn is_parameter(&self) -> bool { self.flags.contains(SymbolFlags::IS_PARAMETER) } - pub(super) fn mark_global(&mut self) { + pub fn mark_global(&mut self) { self.insert_flags(SymbolFlags::MARKED_GLOBAL); } - pub(super) fn mark_nonlocal(&mut self) { + pub fn mark_nonlocal(&mut self) { self.insert_flags(SymbolFlags::MARKED_NONLOCAL); } - pub(super) fn mark_bound(&mut self) { + pub fn mark_bound(&mut self) { if self.is_bound() || self.is_used() { self.insert_flags(SymbolFlags::IS_REASSIGNED); } @@ -137,15 +137,15 @@ impl Symbol { self.insert_flags(SymbolFlags::IS_BOUND); } - pub(super) fn mark_used(&mut self) { + pub fn mark_used(&mut self) { self.insert_flags(SymbolFlags::IS_USED); } - pub(super) fn mark_declared(&mut self) { + pub fn mark_declared(&mut self) { self.insert_flags(SymbolFlags::IS_DECLARED); } - pub(super) fn mark_parameter(&mut self) { + pub fn mark_parameter(&mut self) { self.insert_flags(SymbolFlags::IS_PARAMETER); } @@ -158,7 +158,7 @@ impl Symbol { /// /// Allows lookup by name and a symbol's ID. #[derive(Default, get_size2::GetSize)] -pub(super) struct SymbolTable { +pub struct SymbolTable { symbols: IndexVec, /// Map from symbol name to its ID. @@ -173,7 +173,7 @@ impl SymbolTable { /// ## Panics /// If the ID is not valid for this symbol table. #[track_caller] - pub(crate) fn symbol(&self, id: ScopedSymbolId) -> &Symbol { + pub fn symbol(&self, id: ScopedSymbolId) -> &Symbol { &self.symbols[id] } @@ -182,19 +182,19 @@ impl SymbolTable { /// ## Panics /// If the ID is not valid for this symbol table. #[track_caller] - pub(crate) fn symbol_mut(&mut self, id: ScopedSymbolId) -> &mut Symbol { + pub fn symbol_mut(&mut self, id: ScopedSymbolId) -> &mut Symbol { &mut self.symbols[id] } /// Look up the ID of a symbol by its name. - pub(crate) fn symbol_id(&self, name: &str) -> Option { + pub fn symbol_id(&self, name: &str) -> Option { self.map .find(Self::hash_name(name), |id| self.symbols[*id].name == name) .copied() } /// Iterate over the symbols in this symbol table. - pub(crate) fn iter(&self) -> std::slice::Iter<'_, Symbol> { + pub fn iter(&self) -> std::slice::Iter<'_, Symbol> { self.symbols.iter() } @@ -221,13 +221,13 @@ impl std::fmt::Debug for SymbolTable { } #[derive(Debug, Default)] -pub(super) struct SymbolTableBuilder { +pub struct SymbolTableBuilder { table: SymbolTable, } impl SymbolTableBuilder { /// Add a new symbol to this scope or update the flags if a symbol with the same name already exists. - pub(super) fn add(&mut self, mut symbol: Symbol) -> (ScopedSymbolId, bool) { + pub fn add(&mut self, mut symbol: Symbol) -> (ScopedSymbolId, bool) { let hash = SymbolTable::hash_name(symbol.name()); let entry = self.table.map.entry( hash, @@ -254,7 +254,7 @@ impl SymbolTableBuilder { } } - pub(super) fn build(self) -> SymbolTable { + pub fn build(self) -> SymbolTable { let mut table = self.table; table.symbols.shrink_to_fit(); table diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index 65d71ba332e97..6d591b8228756 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -268,8 +268,8 @@ use crate::types::{PossiblyNarrowedPlaces, Truthiness, Type}; mod place_state; -pub(super) use place_state::PreviousDefinitions; -pub(crate) use place_state::{LiveBinding, ScopedDefinitionId}; +pub use place_state::PreviousDefinitions; +pub use place_state::{LiveBinding, ScopedDefinitionId}; /// Uniquely identifies an interned [`Bindings`] entry in [`UseDefMap::interned_bindings`]. #[newtype_index] @@ -302,7 +302,7 @@ enum InternedEnclosingSnapshotId { /// Applicable definitions and constraints for every use of a name. #[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct UseDefMap<'db> { +pub struct UseDefMap<'db> { /// Array of [`Definition`] in this scope. Only the first entry should be [`DefinitionState::Undefined`]; /// this represents the implicit "unbound"/"undeclared" definition of every place. all_definitions: IndexVec>, @@ -380,16 +380,13 @@ pub(crate) struct UseDefMap<'db> { end_of_scope_reachability: ScopedReachabilityConstraintId, } -pub(crate) enum ApplicableConstraints<'map, 'db> { +pub enum ApplicableConstraints<'map, 'db> { UnboundBinding(NarrowingEvaluator<'map, 'db>), ConstrainedBindings(BindingWithConstraintsIterator<'map, 'db>), } impl<'db> UseDefMap<'db> { - pub(crate) fn bindings_at_use( - &self, - use_id: ScopedUseId, - ) -> BindingWithConstraintsIterator<'_, 'db> { + pub fn bindings_at_use(&self, use_id: ScopedUseId) -> BindingWithConstraintsIterator<'_, 'db> { let bindings_id = self.bindings_by_use[use_id]; self.bindings_iterator( &self.interned_bindings[bindings_id], @@ -397,7 +394,7 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn applicable_constraints( + pub fn applicable_constraints( &self, constraint_key: ConstraintKey, enclosing_scope: FileScopeId, @@ -428,7 +425,7 @@ impl<'db> UseDefMap<'db> { } } - pub(crate) fn is_reachable( + pub fn is_reachable( &self, db: &dyn crate::Db, reachability: ScopedReachabilityConstraintId, @@ -436,7 +433,7 @@ impl<'db> UseDefMap<'db> { self.evaluate_reachability(db, reachability).may_be_true() } - pub(crate) fn evaluate_reachability( + pub fn evaluate_reachability( &self, db: &dyn crate::Db, reachability: ScopedReachabilityConstraintId, @@ -445,11 +442,11 @@ impl<'db> UseDefMap<'db> { .evaluate(db, &self.predicates, reachability) } - pub(crate) fn definition(&self, id: ScopedDefinitionId) -> DefinitionState<'db> { + pub fn definition(&self, id: ScopedDefinitionId) -> DefinitionState<'db> { self.all_definitions[id] } - pub(crate) fn narrowing_evaluator( + pub fn narrowing_evaluator( &self, constraint: ScopedNarrowingConstraint, ) -> NarrowingEvaluator<'_, 'db> { @@ -465,7 +462,7 @@ impl<'db> UseDefMap<'db> { /// be unreachable. Use [`super::SemanticIndex::is_node_reachable`] for the global /// analysis. #[track_caller] - pub(super) fn is_node_reachable(&self, db: &dyn crate::Db, node_key: NodeKey) -> bool { + pub fn is_node_reachable(&self, db: &dyn crate::Db, node_key: NodeKey) -> bool { self .reachability_constraints .evaluate( @@ -479,7 +476,7 @@ impl<'db> UseDefMap<'db> { .may_be_true() } - pub(crate) fn end_of_scope_bindings( + pub fn end_of_scope_bindings( &self, place: ScopedPlaceId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -489,7 +486,7 @@ impl<'db> UseDefMap<'db> { } } - pub(crate) fn end_of_scope_symbol_bindings( + pub fn end_of_scope_symbol_bindings( &self, symbol: ScopedSymbolId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -499,7 +496,7 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn end_of_scope_member_bindings( + pub fn end_of_scope_member_bindings( &self, member: ScopedMemberId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -510,7 +507,7 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn reachable_bindings( + pub fn reachable_bindings( &self, place: ScopedPlaceId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -520,7 +517,7 @@ impl<'db> UseDefMap<'db> { } } - pub(crate) fn reachable_symbol_bindings( + pub fn reachable_symbol_bindings( &self, symbol: ScopedSymbolId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -528,7 +525,7 @@ impl<'db> UseDefMap<'db> { self.bindings_iterator(bindings, BoundnessAnalysis::AssumeBound) } - pub(crate) fn reachable_member_bindings( + pub fn reachable_member_bindings( &self, member: ScopedMemberId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -536,7 +533,7 @@ impl<'db> UseDefMap<'db> { self.bindings_iterator(bindings, BoundnessAnalysis::AssumeBound) } - pub(crate) fn enclosing_snapshot( + pub fn enclosing_snapshot( &self, snapshot_id: ScopedEnclosingSnapshotId, nested_laziness: ScopeLaziness, @@ -564,7 +561,7 @@ impl<'db> UseDefMap<'db> { } } - pub(crate) fn bindings_at_definition( + pub fn bindings_at_definition( &self, definition: Definition<'db>, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -575,7 +572,7 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn declarations_at_binding( + pub fn declarations_at_binding( &self, binding: Definition<'db>, ) -> DeclarationsIterator<'_, 'db> { @@ -586,7 +583,7 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn end_of_scope_declarations<'map>( + pub fn end_of_scope_declarations<'map>( &'map self, place: ScopedPlaceId, ) -> DeclarationsIterator<'map, 'db> { @@ -596,7 +593,7 @@ impl<'db> UseDefMap<'db> { } } - pub(crate) fn end_of_scope_symbol_declarations<'map>( + pub fn end_of_scope_symbol_declarations<'map>( &'map self, symbol: ScopedSymbolId, ) -> DeclarationsIterator<'map, 'db> { @@ -604,7 +601,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::BasedOnUnboundVisibility) } - pub(crate) fn end_of_scope_member_declarations<'map>( + pub fn end_of_scope_member_declarations<'map>( &'map self, member: ScopedMemberId, ) -> DeclarationsIterator<'map, 'db> { @@ -613,7 +610,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::BasedOnUnboundVisibility) } - pub(crate) fn reachable_symbol_declarations( + pub fn reachable_symbol_declarations( &self, symbol: ScopedSymbolId, ) -> DeclarationsIterator<'_, 'db> { @@ -621,7 +618,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::AssumeBound) } - pub(crate) fn reachable_member_declarations( + pub fn reachable_member_declarations( &self, member: ScopedMemberId, ) -> DeclarationsIterator<'_, 'db> { @@ -629,17 +626,14 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::AssumeBound) } - pub(crate) fn reachable_declarations( - &self, - place: ScopedPlaceId, - ) -> DeclarationsIterator<'_, 'db> { + pub fn reachable_declarations(&self, place: ScopedPlaceId) -> DeclarationsIterator<'_, 'db> { match place { ScopedPlaceId::Symbol(symbol) => self.reachable_symbol_declarations(symbol), ScopedPlaceId::Member(member) => self.reachable_member_declarations(member), } } - pub(crate) fn all_end_of_scope_symbol_declarations<'map>( + pub fn all_end_of_scope_symbol_declarations<'map>( &'map self, ) -> impl Iterator)> + 'map { self.end_of_scope_symbols @@ -647,7 +641,7 @@ impl<'db> UseDefMap<'db> { .map(|symbol_id| (symbol_id, self.end_of_scope_symbol_declarations(symbol_id))) } - pub(crate) fn all_end_of_scope_symbol_bindings<'map>( + pub fn all_end_of_scope_symbol_bindings<'map>( &'map self, ) -> impl Iterator)> + 'map { @@ -656,7 +650,7 @@ impl<'db> UseDefMap<'db> { .map(|symbol_id| (symbol_id, self.end_of_scope_symbol_bindings(symbol_id))) } - pub(crate) fn all_reachable_symbols<'map>( + pub fn all_reachable_symbols<'map>( &'map self, ) -> impl Iterator< Item = ( @@ -681,14 +675,14 @@ impl<'db> UseDefMap<'db> { } /// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`. - pub(crate) fn can_implicitly_return_none(&self, db: &dyn crate::Db) -> bool { + pub fn can_implicitly_return_none(&self, db: &dyn crate::Db) -> bool { !self .reachability_constraints .evaluate(db, &self.predicates, self.end_of_scope_reachability) .is_always_false() } - pub(crate) fn binding_reachability( + pub fn binding_reachability( &self, db: &dyn crate::Db, binding: &BindingWithConstraints<'_, 'db>, @@ -739,18 +733,18 @@ impl<'db> UseDefMap<'db> { /// There is a unique ID for each distinct [`EnclosingSnapshotKey`] in the file. #[newtype_index] #[derive(get_size2::GetSize)] -pub(crate) struct ScopedEnclosingSnapshotId; +pub struct ScopedEnclosingSnapshotId; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] -pub(crate) struct EnclosingSnapshotKey { +pub struct EnclosingSnapshotKey { /// The enclosing scope containing the bindings - pub(crate) enclosing_scope: FileScopeId, + pub enclosing_scope: FileScopeId, /// The referenced place (in the enclosing scope) - pub(crate) enclosing_place: ScopedPlaceId, + pub enclosing_place: ScopedPlaceId, /// The nested scope containing the reference - pub(crate) nested_scope: FileScopeId, + pub nested_scope: FileScopeId, /// Laziness of the nested scope (technically redundant, but convenient to have here) - pub(crate) nested_laziness: ScopeLaziness, + pub nested_laziness: ScopeLaziness, } /// Snapshots of enclosing scope place states for resolving a reference in a nested scope. @@ -760,11 +754,11 @@ pub(crate) struct EnclosingSnapshotKey { type EnclosingSnapshots = IndexVec; #[derive(Clone, Debug)] -pub(crate) struct BindingWithConstraintsIterator<'map, 'db> { - pub(crate) all_definitions: &'map IndexVec>, - pub(crate) predicates: &'map Predicates<'db>, - pub(crate) reachability_constraints: &'map ReachabilityConstraints, - pub(crate) boundness_analysis: BoundnessAnalysis, +pub struct BindingWithConstraintsIterator<'map, 'db> { + pub all_definitions: &'map IndexVec>, + pub predicates: &'map Predicates<'db>, + pub reachability_constraints: &'map ReachabilityConstraints, + pub boundness_analysis: BoundnessAnalysis, inner: LiveBindingsIterator<'map>, } @@ -791,20 +785,20 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> { impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {} -pub(crate) struct BindingWithConstraints<'map, 'db> { - pub(crate) binding: DefinitionState<'db>, - pub(crate) narrowing_constraint: NarrowingEvaluator<'map, 'db>, - pub(crate) reachability_constraint: ScopedReachabilityConstraintId, +pub struct BindingWithConstraints<'map, 'db> { + pub binding: DefinitionState<'db>, + pub narrowing_constraint: NarrowingEvaluator<'map, 'db>, + pub reachability_constraint: ScopedReachabilityConstraintId, } -pub(crate) struct NarrowingEvaluator<'map, 'db> { - pub(crate) constraint: ScopedNarrowingConstraint, +pub struct NarrowingEvaluator<'map, 'db> { + pub constraint: ScopedNarrowingConstraint, predicates: &'map Predicates<'db>, reachability_constraints: &'map ReachabilityConstraints, } impl<'db> NarrowingEvaluator<'_, 'db> { - pub(crate) fn narrow( + pub fn narrow( self, db: &'db dyn crate::Db, base_ty: Type<'db>, @@ -821,18 +815,18 @@ impl<'db> NarrowingEvaluator<'_, 'db> { } #[derive(Clone)] -pub(crate) struct DeclarationsIterator<'map, 'db> { +pub struct DeclarationsIterator<'map, 'db> { all_definitions: &'map IndexVec>, - pub(crate) predicates: &'map Predicates<'db>, - pub(crate) reachability_constraints: &'map ReachabilityConstraints, - pub(crate) boundness_analysis: BoundnessAnalysis, + pub predicates: &'map Predicates<'db>, + pub reachability_constraints: &'map ReachabilityConstraints, + pub boundness_analysis: BoundnessAnalysis, inner: LiveDeclarationsIterator<'map>, } #[derive(Debug)] -pub(crate) struct DeclarationWithConstraint<'db> { - pub(crate) declaration: DefinitionState<'db>, - pub(crate) reachability_constraint: ScopedReachabilityConstraintId, +pub struct DeclarationWithConstraint<'db> { + pub declaration: DefinitionState<'db>, + pub reachability_constraint: ScopedReachabilityConstraintId, } impl<'db> Iterator for DeclarationsIterator<'_, 'db> { @@ -863,7 +857,7 @@ struct ReachableDefinitions { /// A snapshot of the definitions and constraints state at a particular point in control flow. #[derive(Clone, Debug)] -pub(super) struct FlowSnapshot { +pub struct FlowSnapshot { symbol_states: IndexVec, member_states: IndexVec, reachability: ScopedReachabilityConstraintId, @@ -871,28 +865,28 @@ pub(super) struct FlowSnapshot { /// A snapshot of the state of a single symbol (e.g. `obj`) and all of its associated members /// (e.g. `obj.attr`, `obj["key"]`). -pub(super) struct SingleSymbolSnapshot { +pub struct SingleSymbolSnapshot { symbol_state: PlaceState, associated_member_states: FxHashMap, } #[derive(Debug)] -pub(super) struct UseDefMapBuilder<'db> { +pub struct UseDefMapBuilder<'db> { /// Append-only array of [`DefinitionState`]. all_definitions: IndexVec>, /// Builder of predicates. - pub(super) predicates: PredicatesBuilder<'db>, + pub predicates: PredicatesBuilder<'db>, /// Builder of reachability constraints. - pub(super) reachability_constraints: ReachabilityConstraintsBuilder, + pub reachability_constraints: ReachabilityConstraintsBuilder, /// Live bindings at each so-far-recorded use. bindings_by_use: IndexVec, /// Tracks whether or not the current point in control flow is reachable from the /// start of the scope. - pub(super) reachability: ScopedReachabilityConstraintId, + pub reachability: ScopedReachabilityConstraintId, /// Tracks whether or not a given AST node is reachable from the start of the scope. node_reachability: FxHashMap, @@ -922,7 +916,7 @@ pub(super) struct UseDefMapBuilder<'db> { } impl<'db> UseDefMapBuilder<'db> { - pub(super) fn new(is_class_scope: bool) -> Self { + pub fn new(is_class_scope: bool) -> Self { Self { all_definitions: IndexVec::from_iter([DefinitionState::Undefined]), predicates: PredicatesBuilder::default(), @@ -941,7 +935,7 @@ impl<'db> UseDefMapBuilder<'db> { } } - pub(super) fn mark_unreachable(&mut self) { + pub fn mark_unreachable(&mut self) { self.reachability = ScopedReachabilityConstraintId::ALWAYS_FALSE; for state in &mut self.symbol_states { @@ -959,7 +953,7 @@ impl<'db> UseDefMapBuilder<'db> { } } - pub(super) fn add_place(&mut self, place: ScopedPlaceId) { + pub fn add_place(&mut self, place: ScopedPlaceId) { match place { ScopedPlaceId::Symbol(symbol) => { let new_place = self @@ -990,11 +984,11 @@ impl<'db> UseDefMapBuilder<'db> { } } - pub(super) fn next_definition_id(&self) -> ScopedDefinitionId { + pub fn next_definition_id(&self) -> ScopedDefinitionId { self.all_definitions.next_index() } - pub(super) fn record_binding( + pub fn record_binding( &mut self, place: ScopedPlaceId, binding: Definition<'db>, @@ -1042,10 +1036,7 @@ impl<'db> UseDefMapBuilder<'db> { ); } - pub(super) fn add_predicate( - &mut self, - predicate: PredicateOrLiteral<'db>, - ) -> ScopedPredicateId { + pub fn add_predicate(&mut self, predicate: PredicateOrLiteral<'db>) -> ScopedPredicateId { match predicate { PredicateOrLiteral::Predicate(predicate) => self.predicates.add_predicate(predicate), PredicateOrLiteral::Literal(true) => ScopedPredicateId::ALWAYS_TRUE, @@ -1054,7 +1045,7 @@ impl<'db> UseDefMapBuilder<'db> { } /// Records a narrowing constraint for only the specified places. - pub(super) fn record_narrowing_constraint_for_places( + pub fn record_narrowing_constraint_for_places( &mut self, predicate: ScopedPredicateId, places: &PossiblyNarrowedPlaces, @@ -1076,7 +1067,7 @@ impl<'db> UseDefMapBuilder<'db> { /// for the negated predicate. This ensures that `atom(P) OR NOT(atom(P))` simplifies to /// `ALWAYS_TRUE` in the TDD, so narrowing is correctly cancelled out after complete /// if/else blocks. - pub(super) fn record_negated_narrowing_constraint_for_places( + pub fn record_negated_narrowing_constraint_for_places( &mut self, predicate: ScopedPredicateId, places: &PossiblyNarrowedPlaces, @@ -1126,7 +1117,7 @@ impl<'db> UseDefMapBuilder<'db> { /// This is only used for `*`-import reachability constraints, which are handled differently /// to most other reachability constraints. See the doc-comment for /// [`Self::record_and_negate_star_import_reachability_constraint`] for more details. - pub(super) fn single_symbol_snapshot( + pub fn single_symbol_snapshot( &self, symbol: ScopedSymbolId, associated_member_ids: &[ScopedMemberId], @@ -1171,7 +1162,7 @@ impl<'db> UseDefMapBuilder<'db> { /// predicate cannot create a terminal statement inside either branch. /// /// [significant regressions]: https://github.com/astral-sh/ruff/pull/17286#issuecomment-2786755746 - pub(super) fn record_and_negate_star_import_reachability_constraint( + pub fn record_and_negate_star_import_reachability_constraint( &mut self, reachability_id: ScopedReachabilityConstraintId, symbol: ScopedSymbolId, @@ -1221,7 +1212,7 @@ impl<'db> UseDefMapBuilder<'db> { /// This is used to gate narrowing by `ReturnsNever` constraints: when a branch contains /// a call to a `NoReturn` function, all narrowing in that branch should be conditional /// on the call actually returning `Never`. - pub(super) fn record_narrowing_constraint_for_all_places( + pub fn record_narrowing_constraint_for_all_places( &mut self, constraint: ScopedNarrowingConstraint, ) { @@ -1234,10 +1225,7 @@ impl<'db> UseDefMapBuilder<'db> { } } - pub(super) fn record_reachability_constraint( - &mut self, - constraint: ScopedReachabilityConstraintId, - ) { + pub fn record_reachability_constraint(&mut self, constraint: ScopedReachabilityConstraintId) { self.reachability = self .reachability_constraints .add_and_constraint(self.reachability, constraint); @@ -1251,11 +1239,7 @@ impl<'db> UseDefMapBuilder<'db> { } } - pub(super) fn record_declaration( - &mut self, - place: ScopedPlaceId, - declaration: Definition<'db>, - ) { + pub fn record_declaration(&mut self, place: ScopedPlaceId, declaration: Definition<'db>) { let def_id = self .all_definitions .push(DefinitionState::Defined(declaration)); @@ -1281,7 +1265,7 @@ impl<'db> UseDefMapBuilder<'db> { ); } - pub(super) fn record_declaration_and_binding( + pub fn record_declaration_and_binding( &mut self, place: ScopedPlaceId, definition: Definition<'db>, @@ -1323,7 +1307,7 @@ impl<'db> UseDefMapBuilder<'db> { ); } - pub(super) fn delete_binding(&mut self, place: ScopedPlaceId) { + pub fn delete_binding(&mut self, place: ScopedPlaceId) { let def_id = self.all_definitions.push(DefinitionState::Deleted); let place_state = match place { ScopedPlaceId::Symbol(symbol) => &mut self.symbol_states[symbol], @@ -1339,12 +1323,7 @@ impl<'db> UseDefMapBuilder<'db> { ); } - pub(super) fn record_use( - &mut self, - place: ScopedPlaceId, - use_id: ScopedUseId, - node_key: NodeKey, - ) { + pub fn record_use(&mut self, place: ScopedPlaceId, use_id: ScopedUseId, node_key: NodeKey) { let bindings = match place { ScopedPlaceId::Symbol(symbol) => &mut self.symbol_states[symbol].bindings(), ScopedPlaceId::Member(member) => &mut self.member_states[member].bindings(), @@ -1359,11 +1338,11 @@ impl<'db> UseDefMapBuilder<'db> { self.record_node_reachability(node_key); } - pub(super) fn record_node_reachability(&mut self, node_key: NodeKey) { + pub fn record_node_reachability(&mut self, node_key: NodeKey) { self.node_reachability.insert(node_key, self.reachability); } - pub(super) fn snapshot_enclosing_state( + pub fn snapshot_enclosing_state( &mut self, enclosing_place: ScopedPlaceId, enclosing_scope: ScopeKind, @@ -1390,7 +1369,7 @@ impl<'db> UseDefMapBuilder<'db> { } } - pub(super) fn update_enclosing_snapshot( + pub fn update_enclosing_snapshot( &mut self, snapshot_id: ScopedEnclosingSnapshotId, enclosing_symbol: ScopedSymbolId, @@ -1411,7 +1390,7 @@ impl<'db> UseDefMapBuilder<'db> { } /// Take a snapshot of the current visible-places state. - pub(super) fn snapshot(&self) -> FlowSnapshot { + pub fn snapshot(&self) -> FlowSnapshot { FlowSnapshot { symbol_states: self.symbol_states.clone(), member_states: self.member_states.clone(), @@ -1422,7 +1401,7 @@ impl<'db> UseDefMapBuilder<'db> { /// Get a snapshot of the current bindings for a place. We use this at the end of loop bodies /// to populate the loop header definitions (bindings in the loop body that are visible via /// loop-back to prior uses in the loop body and also to the loop condition). - pub(super) fn loop_back_bindings( + pub fn loop_back_bindings( &self, place: ScopedPlaceId, ) -> impl Iterator + '_ { @@ -1435,7 +1414,7 @@ impl<'db> UseDefMapBuilder<'db> { } /// Restore the current builder places state to the given snapshot. - pub(super) fn restore(&mut self, snapshot: FlowSnapshot) { + pub fn restore(&mut self, snapshot: FlowSnapshot) { // We never remove places from `place_states` (it's an IndexVec, and the place // IDs must line up), so the current number of known places must always be equal to or // greater than the number of known places in a previously-taken snapshot. @@ -1461,7 +1440,7 @@ impl<'db> UseDefMapBuilder<'db> { /// Merge the given snapshot into the current state, reflecting that we might have taken either /// path to get here. The new state for each place should include definitions from both the /// prior state and the snapshot. - pub(super) fn merge(&mut self, snapshot: FlowSnapshot) { + pub fn merge(&mut self, snapshot: FlowSnapshot) { // As an optimization, if we know statically that either of the snapshots is always // unreachable, we can leave it out of the merged result entirely. Note that we cannot // perform any type inference at this point, so this is largely limited to unreachability @@ -1514,7 +1493,7 @@ impl<'db> UseDefMapBuilder<'db> { .add_or_constraint(self.reachability, snapshot.reachability); } - pub(super) fn finish(mut self) -> UseDefMap<'db> { + pub fn finish(mut self) -> UseDefMap<'db> { self.all_definitions.shrink_to_fit(); self.symbol_states.shrink_to_fit(); self.member_states.shrink_to_fit(); diff --git a/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs b/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs index cddde912af145..ee4adf628f39d 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs @@ -54,7 +54,7 @@ use crate::semantic_index::reachability_constraints::{ /// A newtype-index for a definition in a particular scope. #[newtype_index] #[derive(Ord, PartialOrd, salsa::Update, get_size2::GetSize)] -pub(crate) struct ScopedDefinitionId; +pub struct ScopedDefinitionId; impl ScopedDefinitionId { /// A special ID that is used to describe an implicit start-of-scope state. When @@ -62,9 +62,9 @@ impl ScopedDefinitionId { /// unbound or undeclared at a given usage site. /// When creating a use-def-map builder, we always add an empty `DefinitionState::Undefined` definition /// at index 0, so this ID is always present. - pub(crate) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0); + pub const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0); - pub(crate) fn is_unbound(self) -> bool { + pub fn is_unbound(self) -> bool { self == Self::UNBOUND } } @@ -72,34 +72,34 @@ impl ScopedDefinitionId { /// Live declarations for a single place at some point in control flow, with their /// corresponding reachability constraints. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(super) struct Declarations { +pub struct Declarations { /// A list of live declarations for this place, sorted by their `ScopedDefinitionId` live_declarations: SmallVec<[LiveDeclaration; 2]>, } /// One of the live declarations for a single place at some point in control flow. #[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] -pub(super) struct LiveDeclaration { - pub(super) declaration: ScopedDefinitionId, - pub(super) reachability_constraint: ScopedReachabilityConstraintId, +pub struct LiveDeclaration { + pub declaration: ScopedDefinitionId, + pub reachability_constraint: ScopedReachabilityConstraintId, } -pub(super) type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>; +pub type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>; #[derive(Clone, Copy, Debug)] -pub(in crate::semantic_index) enum PreviousDefinitions { +pub enum PreviousDefinitions { AreShadowed, AreKept, } impl PreviousDefinitions { - pub(super) fn are_shadowed(self) -> bool { + pub fn are_shadowed(self) -> bool { matches!(self, PreviousDefinitions::AreShadowed) } } impl Declarations { - pub(super) fn undeclared(reachability_constraint: ScopedReachabilityConstraintId) -> Self { + pub fn undeclared(reachability_constraint: ScopedReachabilityConstraintId) -> Self { let initial_declaration = LiveDeclaration { declaration: ScopedDefinitionId::UNBOUND, reachability_constraint, @@ -110,7 +110,7 @@ impl Declarations { } /// Record a newly-encountered declaration for this place. - pub(super) fn record_declaration( + pub fn record_declaration( &mut self, declaration: ScopedDefinitionId, reachability_constraint: ScopedReachabilityConstraintId, @@ -127,7 +127,7 @@ impl Declarations { } /// Add given reachability constraint to all live declarations. - pub(super) fn record_reachability_constraint( + pub fn record_reachability_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedReachabilityConstraintId, @@ -139,7 +139,7 @@ impl Declarations { } /// Return an iterator over live declarations for this place. - pub(super) fn iter(&self) -> LiveDeclarationsIterator<'_> { + pub fn iter(&self) -> LiveDeclarationsIterator<'_> { self.live_declarations.iter() } @@ -171,7 +171,7 @@ impl Declarations { } } - pub(super) fn finish(&mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder) { + pub fn finish(&mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder) { self.live_declarations.shrink_to_fit(); for declaration in &self.live_declarations { reachability_constraints.mark_used(declaration.reachability_constraint); @@ -185,7 +185,7 @@ impl Declarations { /// bindings, the current narrowing constraint is necessary for narrowing, so it's stored in /// `Constraint`. #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(super) enum EnclosingSnapshot { +pub enum EnclosingSnapshot { Constraint(ScopedNarrowingConstraint), Bindings(Bindings), } @@ -193,7 +193,7 @@ pub(super) enum EnclosingSnapshot { /// Live bindings for a single place at some point in control flow. Each live binding comes /// with a set of narrowing constraints and a reachability constraint. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(super) struct Bindings { +pub struct Bindings { /// The narrowing constraint applicable to the "unbound" binding, if we need access to it even /// when it's not visible. This happens in class scopes, where local name bindings are not visible /// to nested scopes, but we still need to know what narrowing constraints were applied to the @@ -204,12 +204,12 @@ pub(super) struct Bindings { } impl Bindings { - pub(super) fn unbound_narrowing_constraint(&self) -> ScopedNarrowingConstraint { + pub fn unbound_narrowing_constraint(&self) -> ScopedNarrowingConstraint { self.unbound_narrowing_constraint .unwrap_or(self.live_bindings[0].narrowing_constraint) } - pub(super) fn finish(&mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder) { + pub fn finish(&mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder) { self.live_bindings.shrink_to_fit(); for binding in &self.live_bindings { reachability_constraints.mark_used(binding.reachability_constraint); @@ -220,16 +220,16 @@ impl Bindings { /// One of the live bindings for a single place at some point in control flow. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) struct LiveBinding { - pub(crate) binding: ScopedDefinitionId, - pub(crate) narrowing_constraint: ScopedNarrowingConstraint, - pub(crate) reachability_constraint: ScopedReachabilityConstraintId, +pub struct LiveBinding { + pub binding: ScopedDefinitionId, + pub narrowing_constraint: ScopedNarrowingConstraint, + pub reachability_constraint: ScopedReachabilityConstraintId, } -pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>; +pub type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>; impl Bindings { - pub(super) fn unbound(reachability_constraint: ScopedReachabilityConstraintId) -> Self { + pub fn unbound(reachability_constraint: ScopedReachabilityConstraintId) -> Self { let initial_binding = LiveBinding { binding: ScopedDefinitionId::UNBOUND, narrowing_constraint: ScopedNarrowingConstraint::ALWAYS_TRUE, @@ -242,7 +242,7 @@ impl Bindings { } /// Record a newly-encountered binding for this place. - pub(super) fn record_binding( + pub fn record_binding( &mut self, binding: ScopedDefinitionId, reachability_constraint: ScopedReachabilityConstraintId, @@ -268,7 +268,7 @@ impl Bindings { } /// Add given constraint to all live bindings. - pub(super) fn record_narrowing_constraint( + pub fn record_narrowing_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedNarrowingConstraint, @@ -280,7 +280,7 @@ impl Bindings { } /// Add given reachability constraint to all live bindings. - pub(super) fn record_reachability_constraint( + pub fn record_reachability_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedReachabilityConstraintId, @@ -292,11 +292,11 @@ impl Bindings { } /// Iterate over currently live bindings for this place - pub(super) fn iter(&self) -> LiveBindingsIterator<'_> { + pub fn iter(&self) -> LiveBindingsIterator<'_> { self.live_bindings.iter() } - pub(super) fn merge( + pub fn merge( &mut self, b: Self, reachability_constraints: &mut ReachabilityConstraintsBuilder, @@ -346,14 +346,14 @@ impl Bindings { } #[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] -pub(in crate::semantic_index) struct PlaceState { +pub struct PlaceState { declarations: Declarations, bindings: Bindings, } impl PlaceState { /// Return a new [`PlaceState`] representing an unbound, undeclared place. - pub(super) fn undefined(reachability: ScopedReachabilityConstraintId) -> Self { + pub fn undefined(reachability: ScopedReachabilityConstraintId) -> Self { Self { declarations: Declarations::undeclared(reachability), bindings: Bindings::unbound(reachability), @@ -361,7 +361,7 @@ impl PlaceState { } /// Record a newly-encountered binding for this place. - pub(super) fn record_binding( + pub fn record_binding( &mut self, binding_id: ScopedDefinitionId, reachability_constraint: ScopedReachabilityConstraintId, @@ -380,7 +380,7 @@ impl PlaceState { } /// Add given constraint to all live bindings. - pub(super) fn record_narrowing_constraint( + pub fn record_narrowing_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedNarrowingConstraint, @@ -390,7 +390,7 @@ impl PlaceState { } /// Add given reachability constraint to all live bindings. - pub(super) fn record_reachability_constraint( + pub fn record_reachability_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedReachabilityConstraintId, @@ -402,7 +402,7 @@ impl PlaceState { } /// Record a newly-encountered declaration of this place. - pub(super) fn record_declaration( + pub fn record_declaration( &mut self, declaration_id: ScopedDefinitionId, reachability_constraint: ScopedReachabilityConstraintId, @@ -415,7 +415,7 @@ impl PlaceState { } /// Merge another [`PlaceState`] into this one. - pub(super) fn merge( + pub fn merge( &mut self, b: PlaceState, reachability_constraints: &mut ReachabilityConstraintsBuilder, @@ -425,15 +425,15 @@ impl PlaceState { .merge(b.declarations, reachability_constraints); } - pub(super) fn bindings(&self) -> &Bindings { + pub fn bindings(&self) -> &Bindings { &self.bindings } - pub(super) fn declarations(&self) -> &Declarations { + pub fn declarations(&self) -> &Declarations { &self.declarations } - pub(super) fn finish(&mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder) { + pub fn finish(&mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder) { self.declarations.finish(reachability_constraints); self.bindings.finish(reachability_constraints); } @@ -462,7 +462,7 @@ mod tests { } #[track_caller] - pub(crate) fn assert_declarations(place: &PlaceState, expected: &[&str]) { + pub fn assert_declarations(place: &PlaceState, expected: &[&str]) { let actual = place .declarations() .iter() diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 7f22561f08ad0..339c156abc55f 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -676,7 +676,7 @@ impl HasType for ast::ExceptHandlerExceptHandler { } /// Implemented by types for which the semantic index tracks their scope. -pub(crate) trait HasTrackedScope: HasNodeIndex {} +pub trait HasTrackedScope: HasNodeIndex {} impl HasTrackedScope for ast::Expr {} diff --git a/crates/ty_python_semantic/src/subscript.rs b/crates/ty_python_semantic/src/subscript.rs index b51a9e597b782..c0c3b1d3c8aaf 100644 --- a/crates/ty_python_semantic/src/subscript.rs +++ b/crates/ty_python_semantic/src/subscript.rs @@ -7,9 +7,9 @@ use itertools::Either; use crate::Db; #[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct OutOfBoundsError; +pub struct OutOfBoundsError; -pub(crate) trait PyIndex<'db> { +pub trait PyIndex<'db> { type Item: 'db; fn py_index(self, db: &'db dyn Db, index: i32) -> Result; @@ -41,13 +41,13 @@ enum Position { AfterEnd, } -pub(crate) enum Nth { +pub enum Nth { FromStart(usize), FromEnd(usize), } impl Nth { - pub(crate) fn from_index(index: i32) -> Self { + pub fn from_index(index: i32) -> Self { if index >= 0 { Nth::FromStart(from_nonnegative_i32(index)) } else { @@ -105,9 +105,9 @@ where } #[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct StepSizeZeroError; +pub struct StepSizeZeroError; -pub(crate) trait PySlice<'db> { +pub trait PySlice<'db> { type Item: 'db; fn py_slice( diff --git a/crates/ty_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs index a883a3225a379..7d8f1cd70027d 100644 --- a/crates/ty_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs @@ -74,7 +74,7 @@ declare_lint! { /// /// This rule is skipped if [`analysis.respect-type-ignore-comments`](https://docs.astral.sh/ty/reference/configuration/#respect-type-ignore-comments) /// to `false`. - pub(crate) static UNUSED_TYPE_IGNORE_COMMENT = { + pub static UNUSED_TYPE_IGNORE_COMMENT = { summary: "detects unused `type: ignore` comments", status: LintStatus::stable("0.0.14"), default_level: Level::Warn, @@ -99,7 +99,7 @@ declare_lint! { /// ```py /// a = 20 / 0 # ty: ignore[division-by-zero] /// ``` - pub(crate) static IGNORE_COMMENT_UNKNOWN_RULE = { + pub static IGNORE_COMMENT_UNKNOWN_RULE = { summary: "detects `ty: ignore` comments that reference unknown rules", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Warn, @@ -123,7 +123,7 @@ declare_lint! { /// ```py /// a = 20 / 0 # type: ignore /// ``` - pub(crate) static INVALID_IGNORE_COMMENT = { + pub static INVALID_IGNORE_COMMENT = { summary: "detects ignore comments that use invalid syntax", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Warn, @@ -135,7 +135,7 @@ pub fn is_unused_ignore_comment_lint(name: LintName) -> bool { } #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { +pub fn suppressions(db: &dyn Db, file: File) -> Suppressions { let parsed = parsed_module(db, file).load(db); let source = source_text(db, file); @@ -190,7 +190,7 @@ pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { builder.finish() } -pub(crate) fn check_suppressions( +pub fn check_suppressions( db: &dyn Db, file: File, diagnostics: TypeCheckDiagnostics, @@ -290,7 +290,7 @@ impl<'a> CheckSuppressionsContext<'a> { /// /// This type exists to separate the phases of "check if a diagnostic should /// be reported" and "build the actual diagnostic." -pub(crate) struct SuppressionDiagnosticGuardBuilder<'ctx, 'db> { +pub struct SuppressionDiagnosticGuardBuilder<'ctx, 'db> { ctx: &'ctx CheckSuppressionsContext<'db>, id: DiagnosticId, range: TextRange, @@ -320,10 +320,7 @@ impl<'ctx, 'db> SuppressionDiagnosticGuardBuilder<'ctx, 'db> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - pub(crate) fn into_diagnostic( - self, - message: impl IntoDiagnosticMessage, - ) -> DiagnosticGuard<'ctx> { + pub fn into_diagnostic(self, message: impl IntoDiagnosticMessage) -> DiagnosticGuard<'ctx> { let mut diag = Diagnostic::new(self.id, self.severity, message); let primary_span = Span::from(self.ctx.file).with_range(self.range); @@ -334,7 +331,7 @@ impl<'ctx, 'db> SuppressionDiagnosticGuardBuilder<'ctx, 'db> { /// The suppressions of a single file. #[derive(Debug, Eq, PartialEq, get_size2::GetSize)] -pub(crate) struct Suppressions { +pub struct Suppressions { /// Suppressions that apply to the entire file. /// /// The suppressions are sorted by [`Suppression::comment_range`] and the [`Suppression::suppressed_range`] @@ -358,7 +355,7 @@ pub(crate) struct Suppressions { } impl Suppressions { - pub(crate) fn find_suppression(&self, range: TextRange, id: LintId) -> Option<&Suppression> { + pub fn find_suppression(&self, range: TextRange, id: LintId) -> Option<&Suppression> { self.lint_suppressions(range, id).next() } @@ -409,7 +406,7 @@ impl Suppressions { } } -pub(crate) type SuppressionsIter<'a> = +pub type SuppressionsIter<'a> = std::iter::Chain, std::slice::Iter<'a, Suppression>>; impl<'a> IntoIterator for &'a Suppressions { @@ -427,7 +424,7 @@ impl<'a> IntoIterator for &'a Suppressions { /// create multiple suppressions: one for every code. /// They all share the same `comment_range`. #[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)] -pub(crate) struct Suppression { +pub struct Suppression { target: SuppressionTarget, kind: SuppressionKind, @@ -467,7 +464,7 @@ impl Suppression { } } - pub(crate) fn id(&self) -> FileSuppressionId { + pub fn id(&self) -> FileSuppressionId { FileSuppressionId(self.range) } } @@ -507,7 +504,7 @@ impl fmt::Display for SuppressionKind { /// This is unique enough because it is its exact /// location in the source. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] -pub(crate) struct FileSuppressionId(TextRange); +pub struct FileSuppressionId(TextRange); #[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)] enum SuppressionTarget { diff --git a/crates/ty_python_semantic/src/suppression/parser.rs b/crates/ty_python_semantic/src/suppression/parser.rs index d48550394b99e..6904a118a40b4 100644 --- a/crates/ty_python_semantic/src/suppression/parser.rs +++ b/crates/ty_python_semantic/src/suppression/parser.rs @@ -6,13 +6,13 @@ use ruff_text_size::{TextLen, TextRange, TextSize}; use smallvec::{SmallVec, smallvec}; use thiserror::Error; -pub(super) struct SuppressionParser<'src> { +pub struct SuppressionParser<'src> { cursor: Cursor<'src>, range: TextRange, } impl<'src> SuppressionParser<'src> { - pub(super) fn new(source: &'src str, range: TextRange) -> Self { + pub fn new(source: &'src str, range: TextRange) -> Self { let cursor = Cursor::new(&source[range]); Self { cursor, range } @@ -180,7 +180,7 @@ impl Iterator for SuppressionParser<'_> { /// A single parsed suppression comment. #[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct SuppressionComment { +pub struct SuppressionComment { /// The range of the suppression comment. /// /// This can be a sub-range of the comment token if the comment token contains multiple `#` tokens: @@ -203,25 +203,25 @@ pub(super) struct SuppressionComment { } impl SuppressionComment { - pub(super) fn kind(&self) -> SuppressionKind { + pub fn kind(&self) -> SuppressionKind { self.kind } - pub(super) fn codes(&self) -> Option<&[TextRange]> { + pub fn codes(&self) -> Option<&[TextRange]> { self.codes.as_deref() } - pub(super) fn range(&self) -> TextRange { + pub fn range(&self) -> TextRange { self.range } } #[derive(Debug, Eq, PartialEq, Clone, get_size2::GetSize)] -pub(super) struct ParseError { - pub(super) kind: ParseErrorKind, +pub struct ParseError { + pub kind: ParseErrorKind, /// The position/range at which the parse error occurred. - pub(super) range: TextRange, + pub range: TextRange, } impl ParseError { @@ -239,7 +239,7 @@ impl std::fmt::Display for ParseError { impl Error for ParseError {} #[derive(Debug, Eq, PartialEq, Clone, Error, get_size2::GetSize)] -pub(super) enum ParseErrorKind { +pub enum ParseErrorKind { /// The comment isn't a suppression comment. #[error("not a suppression comment")] NotASuppression, diff --git a/crates/ty_python_semantic/src/suppression/unused.rs b/crates/ty_python_semantic/src/suppression/unused.rs index b8319046e0b9e..47b3ca9b51aa9 100644 --- a/crates/ty_python_semantic/src/suppression/unused.rs +++ b/crates/ty_python_semantic/src/suppression/unused.rs @@ -13,7 +13,7 @@ use crate::suppression::{ /// adds diagnostic for each of them to `diagnostics`. /// /// Does nothing if the [`UNUSED_IGNORE_COMMENT`] rule is disabled. -pub(super) fn check_unused_suppressions(context: &mut CheckSuppressionsContext) { +pub fn check_unused_suppressions(context: &mut CheckSuppressionsContext) { if context.is_lint_disabled(&UNUSED_IGNORE_COMMENT) && context.is_lint_disabled(&UNUSED_TYPE_IGNORE_COMMENT) { diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 79c59608cd147..ba9d4d6411542 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -20,22 +20,22 @@ use smallvec::smallvec_inline; use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; pub use self::cyclic::CycleDetector; -pub(crate) use self::cyclic::TypeTransformer; -pub(crate) use self::diagnostic::register_lints; +pub use self::cyclic::TypeTransformer; +pub use self::diagnostic::register_lints; pub use self::diagnostic::{TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_REFERENCE}; -pub(crate) use self::infer::{ +pub use self::infer::{ TypeContext, infer_complete_scope_types, infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, infer_scope_types, }; pub use self::known_instance::KnownInstanceType; use self::set_theoretic::KnownUnion; -pub(crate) use self::set_theoretic::builder::{IntersectionBuilder, UnionBuilder}; +pub use self::set_theoretic::builder::{IntersectionBuilder, UnionBuilder}; pub use self::set_theoretic::{ IntersectionType, NegativeIntersectionElements, NegativeIntersectionElementsIterator, UnionType, }; pub use self::signatures::ParameterKind; -pub(crate) use self::signatures::Signature; -pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; +pub use self::signatures::Signature; +pub use self::subclass_of::{SubclassOfInner, SubclassOfType}; pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; use crate::place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, builtins_module_scope, @@ -48,8 +48,8 @@ use crate::semantic_index::{imported_modules, place_table, semantic_index}; use crate::suppression::check_suppressions; use crate::types::bound_super::BoundSuperType; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; -pub(crate) use crate::types::callable::{CallableType, CallableTypes}; -pub(crate) use crate::types::class_base::ClassBase; +pub use crate::types::callable::{CallableType, CallableTypes}; +pub use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM}; @@ -62,22 +62,22 @@ use crate::types::function::{ use crate::types::generics::{ ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, }; -pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; +pub use crate::types::generics::{GenericContext, SpecializationBuilder}; use crate::types::infer::InferenceFlags; use crate::types::known_instance::{InternedConstraintSet, InternedType, UnionTypeInstance}; pub use crate::types::method::{BoundMethodType, KnownBoundMethodType, WrapperDescriptorKind}; use crate::types::mro::{MroIterator, StaticMroError}; -pub(crate) use crate::types::narrow::{ +pub use crate::types::narrow::{ NarrowingConstraint, PossiblyNarrowedPlaces, PossiblyNarrowedPlacesBuilder, infer_narrowing_constraint, }; use crate::types::newtype::NewType; -pub(crate) use crate::types::signatures::{Parameter, Parameters}; +pub use crate::types::signatures::{Parameter, Parameters}; use crate::types::signatures::{ParameterForm, walk_signature}; use crate::types::special_form::TypeQualifier; use crate::types::tuple::TupleSpec; use crate::types::type_alias::TypeAliasType; -pub(crate) use crate::types::typed_dict::TypedDictType; +pub use crate::types::typed_dict::TypedDictType; use crate::types::typevar::TypeVarInstance; pub use crate::types::typevar::{ BindingContext, BoundTypeVarInstance, ParamSpecAttrKind, TypeVarBoundOrConstraints, TypeVarKind, @@ -87,63 +87,63 @@ use crate::types::variance::VarianceInferable; use crate::types::visitor::any_over_type; use crate::{Db, FxOrderSet, Program}; pub use class::KnownClass; -pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, StaticClassLiteral}; +pub use class::{ClassLiteral, ClassType, GenericAlias, StaticClassLiteral}; use instance::Protocol; pub use instance::{NominalInstanceType, ProtocolInstanceType}; -pub(crate) use literal::{ +pub use literal::{ BytesLiteralType, EnumLiteralType, LiteralValueType, LiteralValueTypeKind, StringLiteralType, }; pub use special_form::SpecialFormType; -mod bool; -mod bound_super; -mod call; -mod callable; -mod class; -mod class_base; -mod constraints; -mod context; -mod context_manager; -mod cyclic; -mod diagnostic; -mod display; -mod enums; -mod function; -mod generics; +pub mod bool; +pub mod bound_super; +pub mod call; +pub mod callable; +pub mod class; +pub mod class_base; +pub mod constraints; +pub mod context; +pub mod context_manager; +pub mod cyclic; +pub mod diagnostic; +pub mod display; +pub mod enums; +pub mod function; +pub mod generics; pub mod ide_support; -mod infer; -mod instance; -mod iteration; -mod known_instance; +pub mod infer; +pub mod instance; +pub mod iteration; +pub mod known_instance; pub mod list_members; -mod literal; -mod member; -mod method; -mod mro; -mod narrow; -mod newtype; -mod overrides; -mod protocol_class; -pub(crate) mod relation; -mod set_theoretic; -mod signatures; -mod special_form; -mod string_annotation; -mod subclass_of; +pub mod literal; +pub mod member; +pub mod method; +pub mod mro; +pub mod narrow; +pub mod newtype; +pub mod overrides; +pub mod protocol_class; +pub mod relation; +pub mod set_theoretic; +pub mod signatures; +pub mod special_form; +pub mod string_annotation; +pub mod subclass_of; #[cfg(test)] -pub(crate) mod tests; -mod tuple; -mod type_alias; -mod typed_dict; -mod typevar; -mod unpacker; -mod variance; -mod visitor; - -mod definition; +pub mod tests; +pub mod tuple; +pub mod type_alias; +pub mod typed_dict; +pub mod typevar; +pub mod unpacker; +pub mod variance; +pub mod visitor; + +pub mod definition; #[cfg(test)] -mod property_tests; -mod subscript; +pub mod property_tests; +pub mod subscript; pub fn check_types(db: &dyn Db, file: File) -> Vec { let _span = tracing::trace_span!("check_types", ?file).entered(); @@ -189,13 +189,13 @@ pub fn check_types(db: &dyn Db, file: File) -> Vec { } /// Infer the type of a binding. -pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> { +pub fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> { let inference = infer_definition_types(db, definition); inference.binding_type(definition) } /// Infer the type of a declaration. -pub(crate) fn declaration_type<'db>( +pub fn declaration_type<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> TypeAndQualifiers<'db> { @@ -233,17 +233,17 @@ fn definition_expression_type<'db>( } /// A [`TypeTransformer`] that is used in `apply_type_mapping` methods. -pub(crate) type ApplyTypeMappingVisitor<'db> = TypeTransformer<'db, TypeMapping<'db, 'db>>; +pub type ApplyTypeMappingVisitor<'db> = TypeTransformer<'db, TypeMapping<'db, 'db>>; /// A [`CycleDetector`] that is used in `find_legacy_typevars` methods. -pub(crate) type FindLegacyTypeVarsVisitor<'db> = CycleDetector, ()>; +pub type FindLegacyTypeVarsVisitor<'db> = CycleDetector, ()>; #[derive(Debug)] -pub(crate) struct FindLegacyTypeVars; +pub struct FindLegacyTypeVars; /// A [`CycleDetector`] that is used in `visit_specialization` methods. -pub(crate) type SpecializationVisitor<'db> = CycleDetector, ()>; -pub(crate) struct VisitSpecialization; +pub type SpecializationVisitor<'db> = CycleDetector, ()>; +pub struct VisitSpecialization; /// How a generic type has been specialized. /// @@ -278,7 +278,7 @@ impl MaterializationKind { /// method or a `__delete__` method. This enum is used to categorize attributes into two /// groups: (1) data descriptors and (2) normal attributes or non-data descriptors. #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) enum AttributeKind { +pub enum AttributeKind { DataDescriptor, NormalOrNonDataDescriptor, } @@ -305,7 +305,7 @@ enum InstanceFallbackShadowsNonDataDescriptor { bitflags! { #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)] - pub(crate) struct MemberLookupPolicy: u8 { + pub struct MemberLookupPolicy: u8 { /// Dunder methods are looked up on the meta-type of a type without potentially falling /// back on attributes on the type itself. For example, when implicitly invoked on an /// instance, dunder methods are not looked up as instance attributes. And when invoked @@ -344,27 +344,27 @@ impl MemberLookupPolicy { /// If false - Look up the attribute on the meta-type, but fall back to attributes on the instance /// if the meta-type attribute is not found or if the meta-type attribute is not a data /// descriptor. - pub(crate) const fn no_instance_fallback(self) -> bool { + pub const fn no_instance_fallback(self) -> bool { self.contains(Self::NO_INSTANCE_FALLBACK) } /// Exclude attributes defined on `object` when looking up attributes. - pub(crate) const fn mro_no_object_fallback(self) -> bool { + pub const fn mro_no_object_fallback(self) -> bool { self.contains(Self::MRO_NO_OBJECT_FALLBACK) } /// Exclude attributes defined on `type` when looking up meta-class-attributes. - pub(crate) const fn meta_class_no_type_fallback(self) -> bool { + pub const fn meta_class_no_type_fallback(self) -> bool { self.contains(Self::META_CLASS_NO_TYPE_FALLBACK) } /// Exclude attributes defined on `int` or `str` when looking up attributes. - pub(crate) const fn mro_no_int_or_str_fallback(self) -> bool { + pub const fn mro_no_int_or_str_fallback(self) -> bool { self.contains(Self::MRO_NO_INT_OR_STR_LOOKUP) } /// Do not call `__getattr__` during member lookup. - pub(crate) const fn no_getattr_lookup(self) -> bool { + pub const fn no_getattr_lookup(self) -> bool { self.contains(Self::NO_GETATTR_LOOKUP) } } @@ -552,7 +552,7 @@ bitflags! { } } -pub(crate) const DATACLASS_FLAGS: &[(&str, DataclassFlags)] = &[ +pub const DATACLASS_FLAGS: &[(&str, DataclassFlags)] = &[ ("init", DataclassFlags::INIT), ("repr", DataclassFlags::REPR), ("eq", DataclassFlags::EQ), @@ -759,7 +759,7 @@ fn recursive_type_normalize_type_guard_like<'db, T: TypeGuardLike<'db>>( #[salsa::tracked] impl<'db> Type<'db> { - pub(crate) const fn any() -> Self { + pub const fn any() -> Self { Self::Dynamic(DynamicType::Any) } @@ -767,11 +767,11 @@ impl<'db> Type<'db> { Self::Dynamic(DynamicType::Unknown) } - pub(crate) fn divergent(id: salsa::Id) -> Self { + pub fn divergent(id: salsa::Id) -> Self { Self::Dynamic(DynamicType::Divergent(DivergentType { id })) } - pub(crate) const fn is_divergent(&self) -> bool { + pub const fn is_divergent(&self) -> bool { matches!(self, Type::Dynamic(DynamicType::Divergent(_))) } @@ -782,12 +782,12 @@ impl<'db> Type<'db> { ) } - pub(crate) const fn is_never(&self) -> bool { + pub const fn is_never(&self) -> bool { matches!(self, Type::Never) } /// Returns `true` if this type contains a `Self` type variable. - pub(crate) fn contains_self(&self, db: &'db dyn Db) -> bool { + pub fn contains_self(&self, db: &'db dyn Db) -> bool { any_over_type(db, *self, false, |ty| { ty.as_typevar().is_some_and(|tv| tv.typevar(db).is_self(db)) }) @@ -812,7 +812,7 @@ impl<'db> Type<'db> { /// /// Types that defer `Self` binding to call time (functions, bound methods, function-like /// callables) are skipped; see `supports_self_binding`. - pub(crate) fn bind_self_typevars(self, db: &'db dyn Db, self_type: Type<'db>) -> Self { + pub fn bind_self_typevars(self, db: &'db dyn Db, self_type: Type<'db>) -> Self { if !self.supports_self_binding(db) { return self; } @@ -825,16 +825,11 @@ impl<'db> Type<'db> { } /// Returns `true` if `self` is [`Type::Callable`]. - pub(crate) const fn is_callable_type(&self) -> bool { + pub const fn is_callable_type(&self) -> bool { matches!(self, Type::Callable(..)) } - pub(crate) fn cycle_normalized( - self, - db: &'db dyn Db, - previous: Self, - cycle: &salsa::Cycle, - ) -> Self { + pub fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { // When we encounter a salsa cycle, we want to avoid oscillating between two or more types // without converging on a fixed-point result. Most of the time, we union together the // types from each cycle iteration to ensure that our result is monotonic, even if we @@ -937,7 +932,7 @@ impl<'db> Type<'db> { self.is_instance_of(db, KnownClass::NotImplementedType) } - pub(crate) fn is_todo(&self) -> bool { + pub fn is_todo(&self) -> bool { self.as_dynamic().is_some_and(|dynamic| match dynamic { DynamicType::Any | DynamicType::Unknown @@ -960,7 +955,7 @@ impl<'db> Type<'db> { /// /// For example, whereas `` is a generic type, `` /// is a specialization of that type. - pub(crate) fn is_specialized_generic(self, db: &'db dyn Db) -> bool { + pub fn is_specialized_generic(self, db: &'db dyn Db) -> bool { match self { Type::Union(union) => union .elements(db) @@ -993,7 +988,7 @@ impl<'db> Type<'db> { } } - pub(crate) const fn is_dynamic(&self) -> bool { + pub const fn is_dynamic(&self) -> bool { matches!(self, Type::Dynamic(_)) } @@ -1006,7 +1001,7 @@ impl<'db> Type<'db> { /// Currently checks for instances of `types.CoroutineType` (returned by `async def` calls). /// Unions are considered awaitable only if every element is awaitable. /// Intersections are considered awaitable if any positive element is awaitable. - pub(crate) fn is_awaitable(self, db: &'db dyn Db) -> bool { + pub fn is_awaitable(self, db: &'db dyn Db) -> bool { match self { Type::NominalInstance(instance) => { matches!(instance.known_class(db), Some(KnownClass::CoroutineType)) @@ -1046,7 +1041,7 @@ impl<'db> Type<'db> { } /// If the type is a specialized instance of the given `KnownClass`, returns the specialization. - pub(crate) fn known_specialization( + pub fn known_specialization( &self, db: &'db dyn Db, known_class: KnownClass, @@ -1056,7 +1051,7 @@ impl<'db> Type<'db> { } /// If the type is a specialized instance of the given class, returns the specialization. - pub(crate) fn specialization_of( + pub fn specialization_of( self, db: &'db dyn Db, expected_class: StaticClassLiteral<'_>, @@ -1065,7 +1060,7 @@ impl<'db> Type<'db> { } /// If this type is a class instance, returns its specialization. - pub(crate) fn class_specialization(self, db: &'db dyn Db) -> Option> { + pub fn class_specialization(self, db: &'db dyn Db) -> Option> { self.specialization_of_optional(db, None) } @@ -1093,7 +1088,7 @@ impl<'db> Type<'db> { /// Returns the top materialization (or upper bound materialization) of this type, which is the /// most general form of the type that is fully static. #[must_use] - pub(crate) fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> { + pub fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> { self.materialize( db, MaterializationKind::Top, @@ -1104,7 +1099,7 @@ impl<'db> Type<'db> { /// Returns the bottom materialization (or lower bound materialization) of this type, which is /// the most specific form of the type that is fully static. #[must_use] - pub(crate) fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> { + pub fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> { self.materialize( db, MaterializationKind::Bottom, @@ -1170,7 +1165,7 @@ impl<'db> Type<'db> { /// - `materialize()` calls `apply_type_mapping()` (or `apply_type_mapping_impl()`) /// - `materialize_impl()` gets called from `apply_type_mapping()` or from another /// `materialize_impl()` - pub(crate) fn materialize( + pub fn materialize( &self, db: &'db dyn Db, materialization_kind: MaterializationKind, @@ -1184,11 +1179,11 @@ impl<'db> Type<'db> { ) } - pub(crate) fn has_dynamic(self, db: &'db dyn Db) -> bool { + pub fn has_dynamic(self, db: &'db dyn Db) -> bool { any_over_type(db, self, false, |ty| ty.is_dynamic()) } - pub(crate) const fn as_special_form(self) -> Option { + pub const fn as_special_form(self) -> Option { match self { Type::SpecialForm(special_form) => Some(special_form), _ => None, @@ -1202,7 +1197,7 @@ impl<'db> Type<'db> { } } - pub(crate) const fn as_type_alias(self) -> Option> { + pub const fn as_type_alias(self) -> Option> { match self { Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => Some(type_alias), _ => None, @@ -1211,7 +1206,7 @@ impl<'db> Type<'db> { /// If this type is a `Type::TypeAlias`, recursively resolves it to its /// underlying value type. Otherwise, returns `self` unchanged. - pub(crate) fn resolve_type_alias(self, db: &'db dyn Db) -> Type<'db> { + pub fn resolve_type_alias(self, db: &'db dyn Db) -> Type<'db> { let mut ty = self; while let Type::TypeAlias(alias) = ty { ty = alias.value_type(db); @@ -1221,7 +1216,7 @@ impl<'db> Type<'db> { /// Returns `Some(UnionType)` if this type behaves like a union. Apart from explicit unions, /// this returns `Some` for `TypeAlias`es of unions and `NewType`s of `float` and `complex`. - pub(crate) fn as_union_like(self, db: &'db dyn Db) -> Option> { + pub fn as_union_like(self, db: &'db dyn Db) -> Option> { match self.resolve_type_alias(db) { Type::Union(union) => Some(union), Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).as_union_like(db), @@ -1229,32 +1224,32 @@ impl<'db> Type<'db> { } } - pub(crate) const fn as_dynamic(self) -> Option> { + pub const fn as_dynamic(self) -> Option> { match self { Type::Dynamic(dynamic_type) => Some(dynamic_type), _ => None, } } - pub(crate) const fn as_divergent(self) -> Option { + pub const fn as_divergent(self) -> Option { match self { Type::Dynamic(DynamicType::Divergent(divergent)) => Some(divergent), _ => None, } } - pub(crate) const fn as_callable(self) -> Option> { + pub const fn as_callable(self) -> Option> { match self { Type::Callable(callable_type) => Some(callable_type), _ => None, } } - pub(crate) const fn expect_dynamic(self) -> DynamicType<'db> { + pub const fn expect_dynamic(self) -> DynamicType<'db> { self.as_dynamic().expect("Expected a Type::Dynamic variant") } - pub(crate) const fn as_protocol_instance(self) -> Option> { + pub const fn as_protocol_instance(self) -> Option> { match self { Type::ProtocolInstance(instance) => Some(instance), _ => None, @@ -1262,7 +1257,7 @@ impl<'db> Type<'db> { } #[track_caller] - pub(crate) const fn expect_class_literal(self) -> ClassLiteral<'db> { + pub const fn expect_class_literal(self) -> ClassLiteral<'db> { self.as_class_literal() .expect("Expected a Type::ClassLiteral variant") } @@ -1275,25 +1270,25 @@ impl<'db> Type<'db> { matches!(self, Type::ClassLiteral(..)) } - pub(crate) const fn as_literal_value(self) -> Option> { + pub const fn as_literal_value(self) -> Option> { match self { Type::LiteralValue(literal) => Some(literal), _ => None, } } - pub(crate) fn as_literal_value_kind(self) -> Option> { + pub fn as_literal_value_kind(self) -> Option> { match self { Type::LiteralValue(literal) => Some(literal.kind()), _ => None, } } - pub(crate) const fn is_typed_dict(&self) -> bool { + pub const fn is_typed_dict(&self) -> bool { matches!(self, Type::TypedDict(..)) } - pub(crate) const fn as_typed_dict(self) -> Option> { + pub const fn as_typed_dict(self) -> Option> { match self { Type::TypedDict(typed_dict) => Some(typed_dict), _ => None, @@ -1303,7 +1298,7 @@ impl<'db> Type<'db> { /// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`. /// Since a `ClassType` must be specialized, apply the default specialization to any /// unspecialized generic class literal. - pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option> { + pub fn to_class_type(self, db: &'db dyn Db) -> Option> { match self { Type::ClassLiteral(class_literal) => Some(class_literal.default_specialization(db)), Type::GenericAlias(alias) => Some(ClassType::Generic(alias)), @@ -1315,11 +1310,7 @@ impl<'db> Type<'db> { matches!(self, Type::PropertyInstance(..)) } - pub(crate) fn module_literal( - db: &'db dyn Db, - importing_file: File, - submodule: Module<'db>, - ) -> Self { + pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: Module<'db>) -> Self { Self::ModuleLiteral(ModuleLiteralType::new( db, submodule, @@ -1327,18 +1318,18 @@ impl<'db> Type<'db> { )) } - pub(crate) const fn as_module_literal(self) -> Option> { + pub const fn as_module_literal(self) -> Option> { match self { Type::ModuleLiteral(module) => Some(module), _ => None, } } - pub(crate) const fn is_union(self) -> bool { + pub const fn is_union(self) -> bool { matches!(self, Type::Union(_)) } - pub(crate) const fn as_union(self) -> Option> { + pub const fn as_union(self) -> Option> { match self { Type::Union(union_type) => Some(union_type), _ => None, @@ -1346,14 +1337,14 @@ impl<'db> Type<'db> { } #[track_caller] - pub(crate) const fn expect_union(self) -> UnionType<'db> { + pub const fn expect_union(self) -> UnionType<'db> { self.as_union().expect("Expected a Type::Union variant") } /// Returns whether this is a "real" intersection type. (Negated types are represented by an /// intersection containing a single negative branch, which this method does _not_ consider a /// "real" intersection.) - pub(crate) fn is_nontrivial_intersection(self, db: &'db dyn Db) -> bool { + pub fn is_nontrivial_intersection(self, db: &'db dyn Db) -> bool { match self { Type::Intersection(intersection) => !intersection.is_simple_negation(db), _ => false, @@ -1361,7 +1352,7 @@ impl<'db> Type<'db> { } /// Returns the number of union clauses in this type. If the type is not a union, returns 1. - pub(crate) fn union_size(self, db: &'db dyn Db) -> usize { + pub fn union_size(self, db: &'db dyn Db) -> usize { match self { Type::Union(union_type) => union_type.elements(db).len(), Type::Never => 0, @@ -1372,7 +1363,7 @@ impl<'db> Type<'db> { /// Returns the number of intersection clauses in this type. If the type is a union, this is /// the maximum of the `intersection_size` of each union element. If the type is not a union /// nor an intersection, returns 1. - pub(crate) fn intersection_size(self, db: &'db dyn Db) -> usize { + pub fn intersection_size(self, db: &'db dyn Db) -> usize { match self { Type::Intersection(intersection) => { intersection.positive(db).len() + intersection.negative(db).len() @@ -1387,7 +1378,7 @@ impl<'db> Type<'db> { } } - pub(crate) const fn as_function_literal(self) -> Option> { + pub const fn as_function_literal(self) -> Option> { match self { Type::FunctionLiteral(function_type) => Some(function_type), _ => None, @@ -1396,30 +1387,30 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_function_literal(self) -> FunctionType<'db> { + pub fn expect_function_literal(self) -> FunctionType<'db> { self.as_function_literal() .expect("Expected a Type::FunctionLiteral variant") } - pub(crate) const fn is_function_literal(&self) -> bool { + pub const fn is_function_literal(&self) -> bool { matches!(self, Type::FunctionLiteral(..)) } - pub(crate) fn as_string_literal(self) -> Option> { + pub fn as_string_literal(self) -> Option> { match self { Type::LiteralValue(literal) => literal.as_string(), _ => None, } } - pub(crate) fn as_int_literal(self) -> Option { + pub fn as_int_literal(self) -> Option { match self { Type::LiteralValue(literal) => literal.as_int(), _ => None, } } - pub(crate) fn as_enum_literal(self) -> Option> { + pub fn as_enum_literal(self) -> Option> { match self { Type::LiteralValue(literal) => literal.as_enum(), _ => None, @@ -1428,25 +1419,25 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_enum_literal(self) -> EnumLiteralType<'db> { + pub fn expect_enum_literal(self) -> EnumLiteralType<'db> { match self.as_literal_value_kind() { Some(LiteralValueTypeKind::Enum(e)) => e, _ => panic!("Expected a `LiteralValueTypeKind::Enum` variant"), } } - pub(crate) fn is_literal_string(&self) -> bool { + pub fn is_literal_string(&self) -> bool { self.as_literal_value() .is_some_and(literal::LiteralValueType::is_literal_string) } - pub(crate) fn is_string_literal(&self) -> bool { + pub fn is_string_literal(&self) -> bool { self.as_literal_value() .is_some_and(literal::LiteralValueType::is_string) } /// Detects types which are valid to appear inside a `Literal[…]` type annotation. - pub(crate) fn is_literal_or_union_of_literals(&self, db: &'db dyn Db) -> bool { + pub fn is_literal_or_union_of_literals(&self, db: &'db dyn Db) -> bool { match self { Type::Union(union) => union .elements(db) @@ -1465,7 +1456,7 @@ impl<'db> Type<'db> { } } - pub(crate) fn is_union_of_single_valued(&self, db: &'db dyn Db) -> bool { + pub fn is_union_of_single_valued(&self, db: &'db dyn Db) -> bool { let ty = self.resolve_type_alias(db); ty.as_union().is_some_and(|union| { union.elements(db).iter().all(|ty| { @@ -1479,7 +1470,7 @@ impl<'db> Type<'db> { || (ty.is_enum(db) && !ty.overrides_equality(db)) } - pub(crate) fn is_union_with_single_valued(&self, db: &'db dyn Db) -> bool { + pub fn is_union_with_single_valued(&self, db: &'db dyn Db) -> bool { let ty = self.resolve_type_alias(db); ty.as_union().is_some_and(|union| { union.elements(db).iter().any(|ty| { @@ -1494,24 +1485,24 @@ impl<'db> Type<'db> { } /// Create a promotable string literal. - pub(crate) fn string_literal(db: &'db dyn Db, string: &str) -> Self { + pub fn string_literal(db: &'db dyn Db, string: &str) -> Self { Self::LiteralValue(LiteralValueType::promotable(StringLiteralType::new( db, string, ))) } /// Create a promotable enum literal. - pub(crate) fn enum_literal(value: EnumLiteralType<'db>) -> Self { + pub fn enum_literal(value: EnumLiteralType<'db>) -> Self { Self::LiteralValue(LiteralValueType::promotable(value)) } /// Create a promotable integer literal. - pub(crate) fn int_literal(int: i64) -> Self { + pub fn int_literal(int: i64) -> Self { Self::LiteralValue(LiteralValueType::promotable(int)) } /// Create a promotable single-character string literal. - pub(crate) fn single_char_string_literal(db: &'db dyn Db, c: char) -> Self { + pub fn single_char_string_literal(db: &'db dyn Db, c: char) -> Self { Self::LiteralValue(LiteralValueType::promotable(StringLiteralType::new( db, c.to_compact_string(), @@ -1519,7 +1510,7 @@ impl<'db> Type<'db> { } /// Create a promotable bytes literal. - pub(crate) fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self { + pub fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self { Self::LiteralValue(LiteralValueType::promotable(BytesLiteralType::new( db, bytes, ))) @@ -1531,19 +1522,19 @@ impl<'db> Type<'db> { } /// Create a `LiteralString`. - pub(crate) fn literal_string() -> Self { + pub fn literal_string() -> Self { // Note that `LiteralString`s are never implicitly inferred, and so are always unpromotable. Self::LiteralValue(LiteralValueType::unpromotable( LiteralValueTypeKind::LiteralString, )) } - pub(crate) fn typed_dict(defining_class: impl Into>) -> Self { + pub fn typed_dict(defining_class: impl Into>) -> Self { Self::TypedDict(TypedDictType::new(defining_class.into())) } #[must_use] - pub(crate) fn negate(&self, db: &'db dyn Db) -> Type<'db> { + pub fn negate(&self, db: &'db dyn Db) -> Type<'db> { // Avoid invoking the `IntersectionBuilder` for negations that are trivial. // // We verify that this always produces the same result as @@ -1594,13 +1585,13 @@ impl<'db> Type<'db> { } #[must_use] - pub(crate) fn negate_if(&self, db: &'db dyn Db, yes: bool) -> Type<'db> { + pub fn negate_if(&self, db: &'db dyn Db, yes: bool) -> Type<'db> { if yes { self.negate(db) } else { *self } } /// Return `true` if it is possible to spell an equivalent type to this one /// in user annotations without nonstandard extensions to the type system - pub(crate) fn is_spellable(&self, db: &'db dyn Db) -> bool { + pub fn is_spellable(&self, db: &'db dyn Db) -> bool { match self { Type::LiteralValue(_) | Type::Never @@ -1645,11 +1636,7 @@ impl<'db> Type<'db> { /// based on the provided predicate. /// /// Otherwise, returns the type unchanged. - pub(crate) fn filter_union( - self, - db: &'db dyn Db, - f: impl FnMut(&Type<'db>) -> bool, - ) -> Type<'db> { + pub fn filter_union(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { if let Type::Union(union) = self.resolve_type_alias(db) { union.filter(db, f) } else { @@ -1660,7 +1647,7 @@ impl<'db> Type<'db> { /// If the type is a union, removes union elements that are disjoint from `target`. /// /// Otherwise, returns the type unchanged. - pub(crate) fn filter_disjoint_elements( + pub fn filter_disjoint_elements( self, db: &'db dyn Db, target: Type<'db>, @@ -1676,7 +1663,7 @@ impl<'db> Type<'db> { /// Returns the fallback instance type that a literal is an instance of, or `None` if the type /// is not a literal. - pub(crate) fn literal_fallback_instance(self, db: &'db dyn Db) -> Option> { + pub fn literal_fallback_instance(self, db: &'db dyn Db) -> Option> { // There are other literal types that could conceivable be included here: class literals // falling back to `type[X]`, for instance. For now, there is not much rigorous thought put // into what's included vs not; this is just an empirical choice that makes our ecosystem @@ -1694,7 +1681,7 @@ impl<'db> Type<'db> { /// Note that this function tries to promote literals to a more user-friendly form than their /// fallback instance type. For example, `def _() -> int` is promoted to `Callable[[], int]`, /// as opposed to `FunctionType`. - pub(crate) fn promote(self, db: &'db dyn Db) -> Type<'db> { + pub fn promote(self, db: &'db dyn Db) -> Type<'db> { self.apply_type_mapping( db, &TypeMapping::Promote(PromotionMode::On), @@ -1726,7 +1713,7 @@ impl<'db> Type<'db> { /// If this continues, the query will not converge, so this method is called in the cycle recovery function. /// Then `tuple[tuple[Divergent, Literal[1]], Literal[1]]` is replaced with `tuple[Divergent, Literal[1]]` and the query converges. #[must_use] - pub(crate) fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { + pub fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { cycle.head_ids().fold(self, |ty, id| { ty.recursive_type_normalized_impl(db, Type::divergent(id), false) .unwrap_or(Type::divergent(id)) @@ -1834,7 +1821,7 @@ impl<'db> Type<'db> { /// /// If a `TypeContext` is provided, it will be narrowed as nested types are visited, if the /// type is a specialized instance of the same class. - pub(crate) fn visit_specialization(self, db: &'db dyn Db, tcx: TypeContext<'db>, mut f: F) + pub fn visit_specialization(self, db: &'db dyn Db, tcx: TypeContext<'db>, mut f: F) where F: FnMut(BoundTypeVarInstance<'db>, Type<'db>, TypeVarVariance, TypeContext<'db>), { @@ -1936,7 +1923,7 @@ impl<'db> Type<'db> { /// /// Note: This function aims to have no false positives, but might return `false` /// for more complicated types that are actually singletons. - pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool { + pub fn is_singleton(self, db: &'db dyn Db) -> bool { match self { Type::Dynamic(_) | Type::Never => false, @@ -2059,7 +2046,7 @@ impl<'db> Type<'db> { } /// Return true if this type is non-empty and all inhabitants of this type compare equal. - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { + pub fn is_single_valued(self, db: &'db dyn Db) -> bool { match self { Type::FunctionLiteral(..) | Type::BoundMethod(_) @@ -2479,7 +2466,7 @@ impl<'db> Type<'db> { /// /// If `__get__` is not defined on the meta-type, this method returns `None`. #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn try_call_dunder_get( + pub fn try_call_dunder_get( self, db: &'db dyn Db, instance: Option>, @@ -2699,7 +2686,7 @@ impl<'db> Type<'db> { /// Returns whether this type is a data descriptor, i.e. defines `__set__` or `__delete__`. /// If this type is a union, requires all elements of union to be data descriptors. - pub(crate) fn is_data_descriptor(self, d: &'db dyn Db) -> bool { + pub fn is_data_descriptor(self, d: &'db dyn Db) -> bool { self.is_data_descriptor_impl(d, false) } @@ -2710,7 +2697,7 @@ impl<'db> Type<'db> { /// attribute assignment for narrowing if the inferred type of an attribute contains a dynamic type. /// However, strictly applying this rule would disable narrowing too frequently. /// Therefore, for practical convenience, we don't consider dynamic types as data descriptors. - pub(crate) fn may_be_data_descriptor(self, d: &'db dyn Db) -> bool { + pub fn may_be_data_descriptor(self, d: &'db dyn Db) -> bool { self.is_data_descriptor_impl(d, true) } @@ -2879,7 +2866,7 @@ impl<'db> Type<'db> { /// TODO: We should return a `Result` here to handle errors that can appear during attribute /// lookup, like a failed `__get__` call on a descriptor. #[must_use] - pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub fn member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::default()) } @@ -2892,7 +2879,7 @@ impl<'db> Type<'db> { }, heap_size=ruff_memory_usage::heap_size )] - pub(crate) fn member_lookup_with_policy( + pub fn member_lookup_with_policy( self, db: &'db dyn Db, name: Name, @@ -3457,7 +3444,7 @@ impl<'db> Type<'db> { /// elements might be inconsistent, such that there's no argument list that's valid for all /// elements. It's usually best to only worry about "callability" relative to a particular /// argument list, via [`try_call`][Self::try_call] and [`CallErrorKind::NotCallable`]. - fn bindings(self, db: &'db dyn Db) -> Bindings<'db> { + pub fn bindings(self, db: &'db dyn Db) -> Bindings<'db> { match self { Type::Callable(callable) => { CallableBinding::from_overloads(self, callable.signatures(db).iter().cloned()) @@ -4811,7 +4798,7 @@ impl<'db> Type<'db> { } #[must_use] - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> { + pub fn to_instance(self, db: &'db dyn Db) -> Option> { match self { Type::Dynamic(_) | Type::Never => Some(self), Type::ClassLiteral(class) => Some(Type::instance(db, class.default_specialization(db))), @@ -4871,7 +4858,7 @@ impl<'db> Type<'db> { /// /// The `scope_id` and `typevar_binding_context` arguments must always come from the file we are currently inferring, so /// as to avoid cross-module AST dependency. - pub(crate) fn in_type_expression( + pub fn in_type_expression( &self, db: &'db dyn Db, scope_id: ScopeId<'db>, @@ -5075,7 +5062,7 @@ impl<'db> Type<'db> { /// Note: the return type of `type(obj)` is subtly different from this. /// See `Self::dunder_class` for more details. #[must_use] - pub(crate) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { match self { Type::Never => Type::Never, Type::NominalInstance(instance) => instance.to_meta_type(db), @@ -5140,7 +5127,7 @@ impl<'db> Type<'db> { /// this returns `type[dict[str, object]]` instead, because inhabitants of a `TypedDict` are /// instances of `dict` at runtime. #[must_use] - pub(crate) fn dunder_class(self, db: &'db dyn Db) -> Type<'db> { + pub fn dunder_class(self, db: &'db dyn Db) -> Type<'db> { if self.is_typed_dict() { return KnownClass::Dict .to_specialized_class_type(db, &[KnownClass::Str.to_instance(db), Type::object()]) @@ -5153,7 +5140,7 @@ impl<'db> Type<'db> { } #[must_use] - pub(crate) fn apply_optional_specialization( + pub fn apply_optional_specialization( self, db: &'db dyn Db, specialization: Option>, @@ -5178,7 +5165,7 @@ impl<'db> Type<'db> { }, heap_size=ruff_memory_usage::heap_size )] - pub(crate) fn apply_specialization( + pub fn apply_specialization( self, db: &'db dyn Db, specialization: Specialization<'db>, @@ -5455,7 +5442,7 @@ impl<'db> Type<'db> { /// Locates any legacy `TypeVar`s in this type, and adds them to a set. This is used to build /// up a generic context from any legacy `TypeVar`s that appear in a function parameter list or /// `Generic` specialization. - pub(crate) fn find_legacy_typevars( + pub fn find_legacy_typevars( self, db: &'db dyn Db, binding_context: Option>, @@ -5469,7 +5456,7 @@ impl<'db> Type<'db> { ); } - pub(crate) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -5684,7 +5671,7 @@ impl<'db> Type<'db> { /// Bind all unbound legacy type variables to the given context and then /// add all legacy typevars to the provided set. - pub(crate) fn bind_and_find_all_legacy_typevars( + pub fn bind_and_find_all_legacy_typevars( self, db: &'db dyn Db, binding_context: Option>, @@ -5703,7 +5690,7 @@ impl<'db> Type<'db> { } /// Replace default types in parameters of callables with `Unknown`. - pub(crate) fn replace_parameter_defaults(self, db: &'db dyn Db) -> Type<'db> { + pub fn replace_parameter_defaults(self, db: &'db dyn Db) -> Type<'db> { self.apply_type_mapping( db, &TypeMapping::ReplaceParameterDefaults, @@ -5735,7 +5722,7 @@ impl<'db> Type<'db> { /// When not available, this should fall back to the value of `[Type::repr]`. /// Note: this method is used in the builtins `format`, `print`, `str.format` and `f-strings`. #[must_use] - pub(crate) fn str(&self, db: &'db dyn Db) -> Type<'db> { + pub fn str(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Int(_) | LiteralValueTypeKind::Bool(_) => self.repr(db), @@ -5762,7 +5749,7 @@ impl<'db> Type<'db> { /// Return the string representation of this type as it would be provided by the `__repr__` /// method at runtime. #[must_use] - pub(crate) fn repr(&self, db: &'db dyn Db) -> Type<'db> { + pub fn repr(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Int(number) => Type::string_literal(db, &number.to_string()), @@ -5936,7 +5923,7 @@ impl<'db> Type<'db> { } } - pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option> { + pub fn generic_origin(self, db: &'db dyn Db) -> Option> { match self { Type::GenericAlias(generic) => Some(generic.origin(db)), Type::NominalInstance(instance) => { @@ -5953,7 +5940,7 @@ impl<'db> Type<'db> { /// Default-specialize all legacy typevars in this type. /// /// This is used when an implicit type alias is referenced without explicitly specializing it. - pub(crate) fn default_specialize(self, db: &'db dyn Db) -> Type<'db> { + pub fn default_specialize(self, db: &'db dyn Db) -> Type<'db> { let mut variables = FxOrderSet::default(); self.find_legacy_typevars(db, None, &mut variables); let generic_context = GenericContext::from_typevar_instances(db, variables); @@ -6116,17 +6103,17 @@ pub struct SelfBinding<'db> { } impl<'db> SelfBinding<'db> { - pub(crate) fn self_type(&self) -> Type<'db> { + pub fn self_type(&self) -> Type<'db> { self.ty } - pub(crate) fn binding_context(&self) -> Option> { + pub fn binding_context(&self) -> Option> { self.binding_context } } impl<'db> SelfBinding<'db> { - pub(crate) fn new( + pub fn new( db: &'db dyn Db, self_type: Type<'db>, binding_context: Option>, @@ -6215,7 +6202,7 @@ pub enum TypeMapping<'a, 'db> { impl<'db> TypeMapping<'_, 'db> { /// Update the generic context of a [`Signature`] according to the current type mapping - pub(crate) fn update_signature_generic_context( + pub fn update_signature_generic_context( &self, db: &'db dyn Db, context: GenericContext<'db>, @@ -6273,7 +6260,7 @@ impl<'db> TypeMapping<'_, 'db> { } /// Returns a new `TypeMapping` that should be applied in contravariant positions. - pub(crate) fn flip(&self) -> Self { + pub fn flip(&self) -> Self { match self { TypeMapping::Materialize(materialization_kind) => { TypeMapping::Materialize(materialization_kind.flip()) @@ -6358,7 +6345,7 @@ impl DynamicType<'_> { self } - pub(crate) fn is_todo(&self) -> bool { + pub fn is_todo(&self) -> bool { matches!(self, Self::Todo(_) | Self::TodoUnpack) } } @@ -6450,14 +6437,14 @@ impl TypeQualifiers { /// Example: `Annotated[ClassVar[tuple[int]], "metadata"]` would have type `tuple[int]` and the /// qualifier `ClassVar`. #[derive(Clone, Debug, Copy, Eq, PartialEq, salsa::Update, get_size2::GetSize)] -pub(crate) struct TypeAndQualifiers<'db> { +pub struct TypeAndQualifiers<'db> { inner: Type<'db>, origin: TypeOrigin, qualifiers: TypeQualifiers, } impl<'db> TypeAndQualifiers<'db> { - pub(crate) fn new(inner: Type<'db>, origin: TypeOrigin, qualifiers: TypeQualifiers) -> Self { + pub fn new(inner: Type<'db>, origin: TypeOrigin, qualifiers: TypeQualifiers) -> Self { Self { inner, origin, @@ -6465,7 +6452,7 @@ impl<'db> TypeAndQualifiers<'db> { } } - pub(crate) fn declared(inner: Type<'db>) -> Self { + pub fn declared(inner: Type<'db>) -> Self { Self { inner, origin: TypeOrigin::Declared, @@ -6474,29 +6461,26 @@ impl<'db> TypeAndQualifiers<'db> { } /// Forget about type qualifiers and only return the inner type. - pub(crate) fn inner_type(&self) -> Type<'db> { + pub fn inner_type(&self) -> Type<'db> { self.inner } - pub(crate) fn origin(&self) -> TypeOrigin { + pub fn origin(&self) -> TypeOrigin { self.origin } /// Return `self` with an additional qualifier added to the set of qualifiers. - pub(crate) fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { + pub fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { self.qualifiers |= qualifier; self } /// Return the set of type qualifiers. - pub(crate) fn qualifiers(&self) -> TypeQualifiers { + pub fn qualifiers(&self) -> TypeQualifiers { self.qualifiers } - pub(crate) fn map_type( - &self, - f: impl FnOnce(Type<'db>) -> Type<'db>, - ) -> TypeAndQualifiers<'db> { + pub fn map_type(&self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> TypeAndQualifiers<'db> { TypeAndQualifiers { inner: f(self.inner), origin: self.origin, @@ -6857,23 +6841,23 @@ pub enum Truthiness { } impl Truthiness { - pub(crate) const fn is_ambiguous(self) -> bool { + pub const fn is_ambiguous(self) -> bool { matches!(self, Truthiness::Ambiguous) } - pub(crate) const fn is_always_false(self) -> bool { + pub const fn is_always_false(self) -> bool { matches!(self, Truthiness::AlwaysFalse) } - pub(crate) const fn may_be_true(self) -> bool { + pub const fn may_be_true(self) -> bool { !self.is_always_false() } - pub(crate) const fn is_always_true(self) -> bool { + pub const fn is_always_true(self) -> bool { matches!(self, Truthiness::AlwaysTrue) } - pub(crate) const fn negate(self) -> Self { + pub const fn negate(self) -> Self { match self { Self::AlwaysTrue => Self::AlwaysFalse, Self::AlwaysFalse => Self::AlwaysTrue, @@ -6881,11 +6865,11 @@ impl Truthiness { } } - pub(crate) const fn negate_if(self, condition: bool) -> Self { + pub const fn negate_if(self, condition: bool) -> Self { if condition { self.negate() } else { self } } - pub(crate) fn or(self, other: Self) -> Self { + pub fn or(self, other: Self) -> Self { match (self, other) { (Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => Truthiness::AlwaysFalse, (Truthiness::AlwaysTrue, _) | (_, Truthiness::AlwaysTrue) => Truthiness::AlwaysTrue, @@ -7073,19 +7057,19 @@ impl<'db> ModuleLiteralType<'db> { /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(super) struct MetaclassCandidate<'db> { +pub struct MetaclassCandidate<'db> { metaclass: ClassType<'db>, explicit_metaclass_of: StaticClassLiteral<'db>, } /// Information about a `@dataclass_transform`-decorated metaclass. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(super) struct MetaclassTransformInfo<'db> { - pub(super) params: DataclassTransformerParams<'db>, +pub struct MetaclassTransformInfo<'db> { + pub params: DataclassTransformerParams<'db>, /// Whether the metaclass providing these parameters was declared on the class itself /// (via an explicit `metaclass=` keyword) rather than inherited from a base class. - pub(super) from_explicit_metaclass: bool, + pub from_explicit_metaclass: bool, } #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -7108,18 +7092,18 @@ fn walk_typeis_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for TypeIsType<'_> {} impl<'db> TypeIsType<'db> { - pub(crate) fn place_name(self, db: &'db dyn Db) -> Option { + pub fn place_name(self, db: &'db dyn Db) -> Option { let (scope, place) = self.place_info(db)?; let table = place_table(db, scope); Some(format!("{}", table.place(place))) } - pub(crate) fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + pub fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeIs(Self::new(db, ty, None)) } - pub(crate) fn bound( + pub fn bound( db: &'db dyn Db, return_type: Type<'db>, scope: ScopeId<'db>, @@ -7129,21 +7113,16 @@ impl<'db> TypeIsType<'db> { } #[must_use] - pub(crate) fn bind( - self, - db: &'db dyn Db, - scope: ScopeId<'db>, - place: ScopedPlaceId, - ) -> Type<'db> { + pub fn bind(self, db: &'db dyn Db, scope: ScopeId<'db>, place: ScopedPlaceId) -> Type<'db> { Self::bound(db, self.return_type(db), scope, place) } #[must_use] - pub(crate) fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + pub fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeIs(Self::new(db, ty, self.place_info(db))) } - pub(crate) fn is_bound(self, db: &'db dyn Db) -> bool { + pub fn is_bound(self, db: &'db dyn Db) -> bool { self.place_info(db).is_some() } } @@ -7178,18 +7157,18 @@ fn walk_typeguard_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for TypeGuardType<'_> {} impl<'db> TypeGuardType<'db> { - pub(crate) fn place_name(self, db: &'db dyn Db) -> Option { + pub fn place_name(self, db: &'db dyn Db) -> Option { let (scope, place) = self.place_info(db)?; let table = place_table(db, scope); Some(format!("{}", table.place(place))) } - pub(crate) fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + pub fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeGuard(Self::new(db, ty, None)) } - pub(crate) fn bound( + pub fn bound( db: &'db dyn Db, return_type: Type<'db>, scope: ScopeId<'db>, @@ -7199,21 +7178,16 @@ impl<'db> TypeGuardType<'db> { } #[must_use] - pub(crate) fn bind( - self, - db: &'db dyn Db, - scope: ScopeId<'db>, - place: ScopedPlaceId, - ) -> Type<'db> { + pub fn bind(self, db: &'db dyn Db, scope: ScopeId<'db>, place: ScopedPlaceId) -> Type<'db> { Self::bound(db, self.return_type(db), scope, place) } #[must_use] - pub(crate) fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + pub fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeGuard(Self::new(db, ty, self.place_info(db))) } - pub(crate) fn is_bound(self, db: &'db dyn Db) -> bool { + pub fn is_bound(self, db: &'db dyn Db) -> bool { self.place_info(db).is_some() } } @@ -7228,7 +7202,7 @@ impl<'db> VarianceInferable<'db> for TypeGuardType<'db> { /// Common trait for `TypeIs` and `TypeGuard` types that share similar structure /// but have different semantic behaviors. -pub(crate) trait TypeGuardLike<'db>: Copy { +pub trait TypeGuardLike<'db>: Copy { /// The name of this type guard form (for error messages and display) const FORM_NAME: &'static str; @@ -7288,7 +7262,7 @@ impl<'db> TypeGuardLike<'db> for TypeGuardType<'db> { /// Walk the MRO of this class and return the last class just before the specified known base. /// This can be used to determine upper bounds for `Self` type variables on methods that are /// being added to the given class. -pub(super) fn determine_upper_bound<'db>( +pub fn determine_upper_bound<'db>( db: &'db dyn Db, class_literal: StaticClassLiteral<'db>, specialization: Option>, @@ -7304,13 +7278,13 @@ pub(super) fn determine_upper_bound<'db>( } #[derive(Clone, Copy, Debug, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) enum EvaluationMode { +pub enum EvaluationMode { Sync, Async, } impl EvaluationMode { - pub(crate) const fn from_is_async(is_async: bool) -> Self { + pub const fn from_is_async(is_async: bool) -> Self { if is_async { EvaluationMode::Async } else { @@ -7318,7 +7292,7 @@ impl EvaluationMode { } } - pub(crate) const fn is_async(self) -> bool { + pub const fn is_async(self) -> bool { matches!(self, EvaluationMode::Async) } } diff --git a/crates/ty_python_semantic/src/types/bool.rs b/crates/ty_python_semantic/src/types/bool.rs index 954e2614e8bb2..fbf758eefc2d1 100644 --- a/crates/ty_python_semantic/src/types/bool.rs +++ b/crates/ty_python_semantic/src/types/bool.rs @@ -18,7 +18,7 @@ impl<'db> Type<'db> { /// This method should only be used outside type checking or when evaluating if a type /// is truthy or falsy in a context where Python doesn't make an implicit `bool` call. /// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls. - pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness { + pub fn bool(&self, db: &'db dyn Db) -> Truthiness { self.try_bool_impl(db, true, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) .unwrap_or_else(|err| err.fallback_truthiness()) } @@ -29,7 +29,7 @@ impl<'db> Type<'db> { /// when `bool(x)` is called on an object `x`. /// /// Returns an error if the type doesn't implement `__bool__` correctly. - pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result> { + pub fn try_bool(&self, db: &'db dyn Db) -> Result> { self.try_bool_impl(db, false, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) } @@ -319,12 +319,12 @@ impl<'db> Type<'db> { } /// A [`CycleDetector`] that is used in `try_bool` methods. -pub(crate) type TryBoolVisitor<'db> = +pub type TryBoolVisitor<'db> = CycleDetector, Result>>; -pub(crate) struct TryBool; +pub struct TryBool; #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum BoolError<'db> { +pub enum BoolError<'db> { /// The type has a `__bool__` attribute but it can't be called. NotCallable { not_boolable_type: Type<'db> }, @@ -355,7 +355,7 @@ pub(crate) enum BoolError<'db> { } impl<'db> BoolError<'db> { - pub(super) fn fallback_truthiness(&self) -> Truthiness { + pub fn fallback_truthiness(&self) -> Truthiness { match self { BoolError::NotCallable { .. } | BoolError::IncorrectReturnType { .. } @@ -381,7 +381,7 @@ impl<'db> BoolError<'db> { } } - pub(super) fn report_diagnostic(&self, context: &InferContext, condition: impl Ranged) { + pub fn report_diagnostic(&self, context: &InferContext, condition: impl Ranged) { self.report_diagnostic_impl(context, condition.range()); } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 07c6aa12c1d7b..ce8b5b2947ebd 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -23,7 +23,7 @@ use crate::{ /// Enumeration of ways in which a `super()` call can cause us to emit a diagnostic. #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum BoundSuperError<'db> { +pub enum BoundSuperError<'db> { /// The second argument to `super()` (which may have been implicitly provided by /// the Python interpreter) has an abstract or structural type. /// It's impossible to determine whether a `Callable` type or a synthesized protocol @@ -51,7 +51,7 @@ pub(crate) enum BoundSuperError<'db> { } impl<'db> BoundSuperError<'db> { - pub(super) fn report_diagnostic(&self, context: &'db InferContext<'db, '_>, node: AnyNodeRef) { + pub fn report_diagnostic(&self, context: &'db InferContext<'db, '_>, node: AnyNodeRef) { match self { BoundSuperError::AbstractOwnerType { owner_type, @@ -255,7 +255,7 @@ impl<'db> SuperOwnerKind<'db> { } /// Returns the type representation of this owner. - pub(super) fn owner_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn owner_type(self, db: &'db dyn Db) -> Type<'db> { match self { SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic), SuperOwnerKind::Class(class) => class.into(), @@ -278,7 +278,7 @@ pub struct BoundSuperType<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for BoundSuperType<'_> {} -pub(super) fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, bound_super: BoundSuperType<'db>, visitor: &V, @@ -295,7 +295,7 @@ impl<'db> BoundSuperType<'db> { /// - `super(pivot, owner_instance)` is valid only if `isinstance(owner_instance, pivot)` /// /// However, the checking is skipped when any of the arguments is a dynamic type. - pub(super) fn build( + pub fn build( db: &'db dyn Db, pivot_class_type: Type<'db>, owner_type: Type<'db>, @@ -649,7 +649,7 @@ impl<'db> BoundSuperType<'db> { /// The arguments passed to `__get__` depend on whether the owner is an instance or a class. /// See the `CPython` implementation for reference: /// - pub(super) fn try_call_dunder_get_on_attribute( + pub fn try_call_dunder_get_on_attribute( self, db: &'db dyn Db, attribute: PlaceAndQualifiers<'db>, @@ -684,7 +684,7 @@ impl<'db> BoundSuperType<'db> { /// Similar to `Type::find_name_in_mro_with_policy`, but performs lookup starting *after* the /// pivot class in the MRO, based on the `owner` type instead of the `super` type. - pub(super) fn find_name_in_mro_after_pivot( + pub fn find_name_in_mro_after_pivot( self, db: &'db dyn Db, name: &str, @@ -726,7 +726,7 @@ impl<'db> BoundSuperType<'db> { } } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -749,7 +749,7 @@ impl<'db> BoundSuperType<'db> { /// cannot simply delegate to `Type::is_equivalent_to_impl` for this /// case, because `Type::is_equivalent_to_impl` itself delegates back to /// `Type::has_relation_to_impl`, which would cause an infinite loop. - pub(crate) fn is_equivalent_to_impl<'c>( + pub fn is_equivalent_to_impl<'c>( self, db: &'db dyn Db, other: Self, diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index dca255349a9ca..f3e568183464a 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -6,12 +6,12 @@ use crate::types::{MemberLookupPolicy, PropertyInstanceType}; use ruff_python_ast as ast; mod arguments; -pub(crate) mod bind; -pub(super) use arguments::{Argument, CallArguments}; -pub(super) use bind::{Binding, Bindings, CallableBinding, MatchedArgument}; +pub mod bind; +pub use arguments::{Argument, CallArguments}; +pub use bind::{Binding, Bindings, CallableBinding, MatchedArgument}; impl<'db> Type<'db> { - pub(crate) fn try_call_bin_op( + pub fn try_call_bin_op( db: &'db dyn Db, left_ty: Type<'db>, op: ast::Operator, @@ -20,7 +20,7 @@ impl<'db> Type<'db> { Self::try_call_bin_op_with_policy(db, left_ty, op, right_ty, MemberLookupPolicy::default()) } - pub(crate) fn try_call_bin_op_with_policy( + pub fn try_call_bin_op_with_policy( db: &'db dyn Db, left_ty: Type<'db>, op: ast::Operator, @@ -101,14 +101,12 @@ impl<'db> Type<'db> { /// /// The bindings are boxed so that we do not pass around large `Err` variants on the stack. #[derive(Debug)] -pub(crate) struct CallError<'db>(pub(crate) CallErrorKind, pub(crate) Box>); +pub struct CallError<'db>(pub CallErrorKind, pub Box>); impl<'db> CallError<'db> { /// Returns `Some(property)` if the call error was caused by an attempt to set a property /// that has no setter, and `None` otherwise. - pub(crate) fn as_attempt_to_set_property_with_no_setter( - &self, - ) -> Option> { + pub fn as_attempt_to_set_property_with_no_setter(&self) -> Option> { if self.0 != CallErrorKind::BindingError { return None; } @@ -125,7 +123,7 @@ impl<'db> CallError<'db> { /// The reason why calling a type failed. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum CallErrorKind { +pub enum CallErrorKind { /// The type is not callable. For a union type, _none_ of the union elements are callable. NotCallable, @@ -142,7 +140,7 @@ pub(crate) enum CallErrorKind { } #[derive(Debug)] -pub(super) enum CallDunderError<'db> { +pub enum CallDunderError<'db> { /// The dunder attribute exists but it can't be called with the given arguments. /// /// This includes non-callable dunder attributes that are possibly unbound. @@ -158,7 +156,7 @@ pub(super) enum CallDunderError<'db> { } impl<'db> CallDunderError<'db> { - pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { + pub fn return_type(&self, db: &'db dyn Db) -> Option> { match self { Self::MethodNotAvailable | Self::CallError(CallErrorKind::NotCallable, _) => None, Self::CallError(_, bindings) => Some(bindings.return_type(db)), @@ -166,7 +164,7 @@ impl<'db> CallDunderError<'db> { } } - pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> { + pub fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> { self.return_type(db).unwrap_or(Type::unknown()) } } @@ -178,7 +176,7 @@ impl<'db> From> for CallDunderError<'db> { } #[derive(Debug)] -pub(crate) enum CallBinOpError { +pub enum CallBinOpError { /// The dunder attribute exists but it can't be called with the given arguments. /// /// This includes non-callable dunder attributes that are possibly unbound. diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 239c02a3bfe14..d05a4f7d8e358 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -26,7 +26,7 @@ const MAX_TUPLE_EXPANSION: usize = 64; const MAX_TOTAL_EXPANSION: usize = 256; #[derive(Clone, Copy, Debug)] -pub(crate) enum Argument<'a> { +pub enum Argument<'a> { /// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site. Synthetic, /// A positional argument. @@ -41,7 +41,7 @@ pub(crate) enum Argument<'a> { /// Arguments for a single call, in source order, along with inferred types for each argument. #[derive(Clone, Debug, Default)] -pub(crate) struct CallArguments<'a, 'db> { +pub struct CallArguments<'a, 'db> { arguments: Vec>, types: Vec>>, } @@ -55,7 +55,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// Create `CallArguments` from AST arguments. We will use the provided callback to obtain the /// type of each splatted argument, so that we can determine its length. All other arguments /// will remain uninitialized as `Unknown`. - pub(crate) fn from_arguments( + pub fn from_arguments( arguments: &'a ast::Arguments, mut infer_argument_type: impl FnMut(Option<&ast::Expr>, &ast::Expr) -> Type<'db>, ) -> Self { @@ -85,7 +85,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// /// This currently only exists for the LSP usecase, and shouldn't be used in normal /// typechecking. - pub(crate) fn from_arguments_typed( + pub fn from_arguments_typed( arguments: &'a ast::Arguments, mut infer_argument_type: impl FnMut(&ast::Expr) -> Type<'db>, ) -> Self { @@ -115,33 +115,33 @@ impl<'a, 'db> CallArguments<'a, 'db> { } /// Create a [`CallArguments`] with no arguments. - pub(crate) fn none() -> Self { + pub fn none() -> Self { Self::default() } /// Create a [`CallArguments`] from an iterator over non-variadic positional argument types. - pub(crate) fn positional(positional_tys: impl IntoIterator>) -> Self { + pub fn positional(positional_tys: impl IntoIterator>) -> Self { let types: Vec<_> = positional_tys.into_iter().map(Some).collect(); let arguments = vec![Argument::Positional; types.len()]; Self { arguments, types } } - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { self.arguments.len() } - pub(crate) fn types(&self) -> &[Option>] { + pub fn types(&self) -> &[Option>] { &self.types } - pub(crate) fn iter_types(&self) -> impl Iterator> { + pub fn iter_types(&self) -> impl Iterator> { self.types.iter().map(|ty| ty.unwrap_or_else(Type::unknown)) } /// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front /// of this argument list. (If `bound_self` is none, we return the argument list /// unmodified.) - pub(crate) fn with_self(&self, bound_self: Option>) -> Cow<'_, Self> { + pub fn with_self(&self, bound_self: Option>) -> Cow<'_, Self> { if bound_self.is_some() { let arguments = std::iter::once(Argument::Synthetic) .chain(self.arguments.iter().copied()) @@ -155,18 +155,18 @@ impl<'a, 'db> CallArguments<'a, 'db> { } } - pub(crate) fn iter(&self) -> impl Iterator, Option>)> + '_ { + pub fn iter(&self) -> impl Iterator, Option>)> + '_ { (self.arguments.iter().copied()).zip(self.types.iter().copied()) } - pub(crate) fn iter_mut( + pub fn iter_mut( &mut self, ) -> impl Iterator, &mut Option>)> + '_ { (self.arguments.iter().copied()).zip(self.types.iter_mut()) } /// Create a new [`CallArguments`] starting from the specified index. - pub(super) fn start_from(&self, index: usize) -> Self { + pub fn start_from(&self, index: usize) -> Self { Self { arguments: self.arguments[index..].to_vec(), types: self.types[index..].to_vec(), @@ -179,7 +179,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// contains the same arguments, but with one or more of the argument types expanded. /// /// [argument type expansion]: https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion - pub(super) fn expand(&self, db: &'db dyn Db) -> impl Iterator> + '_ { + pub fn expand(&self, db: &'db dyn Db) -> impl Iterator> + '_ { /// Represents the state of the expansion process. enum State<'a, 'b, 'db> { LimitReached(usize), @@ -276,7 +276,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { }) } - pub(super) fn display(&self, db: &'db dyn Db) -> impl Display { + pub fn display(&self, db: &'db dyn Db) -> impl Display { struct DisplayCallArguments<'a, 'db> { call_arguments: &'a CallArguments<'a, 'db>, db: &'db dyn Db, @@ -326,7 +326,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// Represents a single element of the expansion process for argument types for [`expand`]. /// /// [`expand`]: CallArguments::expand -pub(super) enum Expansion<'a, 'db> { +pub enum Expansion<'a, 'db> { /// Indicates that the expansion process has reached the maximum number of argument lists /// that can be generated in a single step. /// @@ -352,7 +352,7 @@ impl<'a, 'db> FromIterator<(Argument<'a>, Option>)> for CallArguments< /// Returns `true` if the type can be expanded into its subtypes. /// /// In other words, it returns `true` if [`expand_type`] returns [`Some`] for the given type. -pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { match ty { Type::NominalInstance(instance) => { let class = instance.class(db); diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index a1019b390f7ac..ce035b191273d 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -174,7 +174,7 @@ impl<'db> BindingsElement<'db> { /// where the call fails. If at least one binding succeeds, the element succeeds. Return types /// are combined using intersection. #[derive(Debug, Clone)] -pub(crate) struct Bindings<'db> { +pub struct Bindings<'db> { /// The type that is (hopefully) callable. callable_type: Type<'db>, @@ -199,7 +199,7 @@ impl<'db> Bindings<'db> { /// Creates a new `Bindings` from an iterator of [`Bindings`]s for a union type. /// Each input `Bindings` becomes a union element, preserving any intersection structure. /// Panics if the iterator is empty. - pub(crate) fn from_union(callable_type: Type<'db>, bindings_iter: I) -> Self + pub fn from_union(callable_type: Type<'db>, bindings_iter: I) -> Self where I: IntoIterator>, { @@ -230,7 +230,7 @@ impl<'db> Bindings<'db> { /// Creates a new `Bindings` from an iterator of [`Bindings`]s for an intersection type. /// All input bindings are combined into a single intersection element. /// Panics if the iterator is empty. - pub(crate) fn from_intersection(callable_type: Type<'db>, bindings_iter: I) -> Self + pub fn from_intersection(callable_type: Type<'db>, bindings_iter: I) -> Self where I: IntoIterator>, { @@ -263,7 +263,7 @@ impl<'db> Bindings<'db> { } } - pub(crate) fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) { + pub fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) { if self.callable_type == before { self.callable_type = after; } @@ -272,10 +272,7 @@ impl<'db> Bindings<'db> { } } - pub(crate) fn with_constructor_instance_type( - mut self, - constructor_instance_type: Type<'db>, - ) -> Self { + pub fn with_constructor_instance_type(mut self, constructor_instance_type: Type<'db>) -> Self { self.constructor_instance_type = Some(constructor_instance_type); for binding in self.iter_flat_mut() { @@ -288,7 +285,7 @@ impl<'db> Bindings<'db> { self } - pub(crate) fn with_generic_context( + pub fn with_generic_context( mut self, db: &'db dyn Db, generic_context: Option>, @@ -308,29 +305,29 @@ impl<'db> Bindings<'db> { self } - pub(crate) fn set_dunder_call_is_possibly_unbound(&mut self) { + pub fn set_dunder_call_is_possibly_unbound(&mut self) { for binding in self.iter_flat_mut() { binding.dunder_call_is_possibly_unbound = true; } } - pub(crate) fn set_implicit_dunder_new_is_possibly_unbound(&mut self) { + pub fn set_implicit_dunder_new_is_possibly_unbound(&mut self) { self.implicit_dunder_new_is_possibly_unbound = true; } - pub(crate) fn set_implicit_dunder_init_is_possibly_unbound(&mut self) { + pub fn set_implicit_dunder_init_is_possibly_unbound(&mut self) { self.implicit_dunder_init_is_possibly_unbound = true; } - pub(crate) fn argument_forms(&self) -> &[Option] { + pub fn argument_forms(&self) -> &[Option] { &self.argument_forms.values } - pub(crate) fn has_implicit_dunder_new_is_possibly_unbound(&self) -> bool { + pub fn has_implicit_dunder_new_is_possibly_unbound(&self) -> bool { self.implicit_dunder_new_is_possibly_unbound } - pub(crate) fn has_implicit_dunder_init_is_possibly_unbound(&self) -> bool { + pub fn has_implicit_dunder_init_is_possibly_unbound(&self) -> bool { self.implicit_dunder_init_is_possibly_unbound } @@ -339,7 +336,7 @@ impl<'db> Bindings<'db> { /// Note: This loses the union/intersection distinction. The returned iterator yields /// all `CallableBinding`s from all elements, which can then be further flattened to /// individual `Binding`s via `CallableBinding`'s `IntoIterator` implementation. - pub(crate) fn iter_flat(&self) -> impl Iterator> { + pub fn iter_flat(&self) -> impl Iterator> { self.elements.iter().flat_map(|e| e.bindings.iter()) } @@ -347,7 +344,7 @@ impl<'db> Bindings<'db> { /// /// Note: This loses the union/intersection distinction. Use only when you need to /// modify all bindings regardless of their union/intersection grouping. - pub(crate) fn iter_flat_mut(&mut self) -> impl Iterator> { + pub fn iter_flat_mut(&mut self) -> impl Iterator> { self.elements.iter_mut().flat_map(|e| e.bindings.iter_mut()) } @@ -356,7 +353,7 @@ impl<'db> Bindings<'db> { /// /// - callable bindings inside an element are intersected /// - elements are unioned - pub(crate) fn map_types( + pub fn map_types( &self, db: &'db dyn Db, mut map: impl FnMut(&CallableBinding<'db>) -> Option>, @@ -378,7 +375,7 @@ impl<'db> Bindings<'db> { UnionType::from_elements(db, element_types) } - pub(crate) fn map(self, f: impl Fn(CallableBinding<'db>) -> CallableBinding<'db>) -> Self { + pub fn map(self, f: impl Fn(CallableBinding<'db>) -> CallableBinding<'db>) -> Self { Self { callable_type: self.callable_type, argument_forms: self.argument_forms, @@ -404,11 +401,7 @@ impl<'db> Bindings<'db> { /// /// Once you have argument types available, you can call [`check_types`][Self::check_types] to /// verify that each argument type is assignable to the corresponding parameter type. - pub(crate) fn match_parameters( - mut self, - db: &'db dyn Db, - arguments: &CallArguments<'_, 'db>, - ) -> Self { + pub fn match_parameters(mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) -> Self { let mut argument_forms = ArgumentForms::new(arguments.len()); for binding in self.iter_flat_mut() { binding.match_parameters(db, arguments, &mut argument_forms); @@ -430,7 +423,7 @@ impl<'db> Bindings<'db> { /// We update the bindings to include the return type of the call, the bound types for all /// parameters, and any errors resulting from binding the call, all for each union element and /// overload (if any). - pub(crate) fn check_types( + pub fn check_types( mut self, db: &'db dyn Db, constraints: &ConstraintSetBuilder<'db>, @@ -450,7 +443,7 @@ impl<'db> Bindings<'db> { } } - pub(crate) fn check_types_impl( + pub fn check_types_impl( &mut self, db: &'db dyn Db, constraints: &ConstraintSetBuilder<'db>, @@ -521,7 +514,7 @@ impl<'db> Bindings<'db> { } /// Returns true if this is a single callable (not a union or intersection). - pub(crate) fn is_single(&self) -> bool { + pub fn is_single(&self) -> bool { match &*self.elements { [single] => single.bindings.len() == 1, _ => false, @@ -529,7 +522,7 @@ impl<'db> Bindings<'db> { } /// Returns the single `CallableBinding` if this is not a union or intersection. - pub(crate) fn single_element(&self) -> Option<&CallableBinding<'db>> { + pub fn single_element(&self) -> Option<&CallableBinding<'db>> { if self.is_single() { self.elements.first().and_then(|e| e.bindings.first()) } else { @@ -537,7 +530,7 @@ impl<'db> Bindings<'db> { } } - pub(crate) fn callable_type(&self) -> Type<'db> { + pub fn callable_type(&self) -> Type<'db> { self.callable_type } @@ -582,7 +575,7 @@ impl<'db> Bindings<'db> { /// Returns the return type of the call. For successful calls, this is the actual return type. /// For calls with binding errors, this is a type that best approximates the return type. For /// types that are not callable, returns `Type::Unknown`. - pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + pub fn return_type(&self, db: &'db dyn Db) -> Type<'db> { if let Some(return_ty) = self.constructor_return_type(db) { return return_ty; } @@ -614,11 +607,7 @@ impl<'db> Bindings<'db> { /// Report diagnostics for all of the errors that occurred when trying to match actual /// arguments to formal parameters. If the callable is a union, or has multiple overloads, we /// report a single diagnostic if we couldn't match any union element or overload. - pub(crate) fn report_diagnostics( - &self, - context: &InferContext<'db, '_>, - node: ast::AnyNodeRef, - ) { + pub fn report_diagnostics(&self, context: &InferContext<'db, '_>, node: ast::AnyNodeRef) { // If all elements are not callable, report that the type as a whole is not callable. if self.elements.iter().all(|e| !e.is_callable()) { if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { @@ -2007,24 +1996,24 @@ impl<'db> From> for Bindings<'db> { /// specific errors that occurred when trying to match them up. If the callable has multiple /// overloads, we store this error information for each overload. #[derive(Debug, Clone)] -pub(crate) struct CallableBinding<'db> { +pub struct CallableBinding<'db> { /// The type that is (hopefully) callable. - pub(crate) callable_type: Type<'db>, + pub callable_type: Type<'db>, /// The type we'll use for error messages referring to details of the called signature. For /// calls to functions this will be the same as `callable_type`; for other callable instances /// it may be a `__call__` method. - pub(crate) signature_type: Type<'db>, + pub signature_type: Type<'db>, /// If this is a callable object (i.e. called via a `__call__` method), the boundness of /// that call method. - pub(crate) dunder_call_is_possibly_unbound: bool, + pub dunder_call_is_possibly_unbound: bool, /// The type of the bound `self` or `cls` parameter if this signature is for a bound method. - pub(crate) bound_type: Option>, + pub bound_type: Option>, /// The type of the instance being constructed, if this signature is for a constructor. - pub(crate) constructor_instance_type: Option>, + pub constructor_instance_type: Option>, /// The return type of this overloaded callable. /// @@ -2061,7 +2050,7 @@ pub(crate) struct CallableBinding<'db> { } impl<'db> CallableBinding<'db> { - pub(crate) fn from_overloads( + pub fn from_overloads( signature_type: Type<'db>, overloads: impl IntoIterator>, ) -> Self { @@ -2081,7 +2070,7 @@ impl<'db> CallableBinding<'db> { } } - pub(crate) fn not_callable(signature_type: Type<'db>) -> Self { + pub fn not_callable(signature_type: Type<'db>) -> Self { Self { callable_type: signature_type, signature_type, @@ -2094,7 +2083,7 @@ impl<'db> CallableBinding<'db> { } } - pub(crate) fn bake_bound_type_into_overloads(&mut self, db: &'db dyn Db) { + pub fn bake_bound_type_into_overloads(&mut self, db: &'db dyn Db) { let Some(bound_self) = self.bound_type.take() else { return; }; @@ -2103,7 +2092,7 @@ impl<'db> CallableBinding<'db> { } } - pub(crate) fn with_bound_type(mut self, bound_type: Type<'db>) -> Self { + pub fn with_bound_type(mut self, bound_type: Type<'db>) -> Self { self.bound_type = Some(bound_type); self } @@ -2718,7 +2707,7 @@ impl<'db> CallableBinding<'db> { Ok(()) } - pub(crate) fn is_callable(&self) -> bool { + pub fn is_callable(&self) -> bool { !self.overloads.is_empty() } @@ -2771,7 +2760,7 @@ impl<'db> CallableBinding<'db> { } /// Returns the index of the matching overload in the form of [`MatchingOverloadIndex`]. - pub(crate) fn matching_overload_index(&self) -> MatchingOverloadIndex { + pub fn matching_overload_index(&self) -> MatchingOverloadIndex { let mut matching_overloads = self.matching_overloads(); match matching_overloads.next() { None => MatchingOverloadIndex::None, @@ -2790,14 +2779,12 @@ impl<'db> CallableBinding<'db> { } /// Returns all overloads for this call binding, including overloads that did not match. - pub(crate) fn overloads(&self) -> &[Binding<'db>] { + pub fn overloads(&self) -> &[Binding<'db>] { self.overloads.as_slice() } /// Returns an iterator over all the overloads that matched for this call binding. - pub(crate) fn matching_overloads( - &self, - ) -> impl Iterator)> + Clone { + pub fn matching_overloads(&self) -> impl Iterator)> + Clone { self.overloads .iter() .enumerate() @@ -2805,9 +2792,7 @@ impl<'db> CallableBinding<'db> { } /// Returns an iterator over all the mutable overloads that matched for this call binding. - pub(crate) fn matching_overloads_mut( - &mut self, - ) -> impl Iterator)> { + pub fn matching_overloads_mut(&mut self) -> impl Iterator)> { self.overloads .iter_mut() .enumerate() @@ -2824,7 +2809,7 @@ impl<'db> CallableBinding<'db> { /// /// For an invalid call to an overloaded function, we return `Type::unknown`, since we cannot /// make any useful conclusions about which overload was intended to be called. - pub(crate) fn return_type(&self) -> Type<'db> { + pub fn return_type(&self) -> Type<'db> { if let Some(overload_call_return_type) = self.overload_call_return_type { return match overload_call_return_type { OverloadCallReturnType::ArgumentTypeExpansion(return_type) => return_type, @@ -3058,7 +3043,7 @@ enum OverloadCallReturnType<'db> { } #[derive(Debug)] -pub(crate) enum MatchingOverloadIndex { +pub enum MatchingOverloadIndex { /// No matching overloads found. None, @@ -4353,23 +4338,23 @@ impl<'db> MatchedArgument<'db> { /// Indicates that a parameter of the given name was not found. #[derive(Debug, Clone, Copy)] -pub(crate) struct UnknownParameterNameError; +pub struct UnknownParameterNameError; /// Binding information for one of the overloads of a callable. #[derive(Debug, Clone)] -pub(crate) struct Binding<'db> { - pub(crate) signature: Signature<'db>, +pub struct Binding<'db> { + pub signature: Signature<'db>, /// The type that is (hopefully) callable. - pub(crate) callable_type: Type<'db>, + pub callable_type: Type<'db>, /// The type we'll use for error messages referring to details of the called signature. For /// calls to functions this will be the same as `callable_type`; for other callable instances /// it may be a `__call__` method. - pub(crate) signature_type: Type<'db>, + pub signature_type: Type<'db>, /// The type of the instance being constructed, if this signature is for a constructor. - pub(crate) constructor_instance_type: Option>, + pub constructor_instance_type: Option>, /// Return type of the call. return_ty: Type<'db>, @@ -4397,7 +4382,7 @@ pub(crate) struct Binding<'db> { } impl<'db> Binding<'db> { - pub(crate) fn single(signature_type: Type<'db>, signature: Signature<'db>) -> Binding<'db> { + pub fn single(signature_type: Type<'db>, signature: Signature<'db>) -> Binding<'db> { Binding { signature, callable_type: signature_type, @@ -4491,17 +4476,17 @@ impl<'db> Binding<'db> { (self.inferable_typevars, self.specialization, self.return_ty) = checker.finish(); } - pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { + pub fn set_return_type(&mut self, return_ty: Type<'db>) { self.return_ty = return_ty; } - pub(crate) fn return_type(&self) -> Type<'db> { + pub fn return_type(&self) -> Type<'db> { self.return_ty } /// Returns the bound types for each parameter, in parameter source order, or `None` if no /// argument was matched to that parameter. - pub(crate) fn parameter_types(&self) -> &[Option>] { + pub fn parameter_types(&self) -> &[Option>] { &self.parameter_tys } @@ -4509,7 +4494,7 @@ impl<'db> Binding<'db> { /// that parameter. /// /// Returns an error if the parameter name is not found. - pub(crate) fn parameter_type_by_name( + pub fn parameter_type_by_name( &self, parameter_name: &str, fallback_to_default: bool, @@ -4532,7 +4517,7 @@ impl<'db> Binding<'db> { } } - pub(crate) fn arguments_for_parameter<'a>( + pub fn arguments_for_parameter<'a>( &'a self, argument_types: &'a CallArguments<'a, 'db>, parameter_index: usize, @@ -4611,15 +4596,15 @@ impl<'db> Binding<'db> { /// Returns a vector where each index corresponds to an argument position, /// and the value is the parameter index that argument maps to (if any). - pub(crate) fn argument_matches(&self) -> &[MatchedArgument<'db>] { + pub fn argument_matches(&self) -> &[MatchedArgument<'db>] { &self.argument_matches } - pub(crate) fn specialization(&self) -> Option> { + pub fn specialization(&self) -> Option> { self.specialization } - pub(crate) fn errors(&self) -> &[BindingError<'db>] { + pub fn errors(&self) -> &[BindingError<'db>] { &self.errors } @@ -4741,16 +4726,13 @@ impl CallableBindingSnapshotter { /// Describes a callable for the purposes of diagnostics. #[derive(Debug)] -pub(crate) struct CallableDescription<'a> { - pub(crate) name: &'a str, - pub(crate) kind: &'a str, +pub struct CallableDescription<'a> { + pub name: &'a str, + pub kind: &'a str, } impl<'db> CallableDescription<'db> { - pub(crate) fn new( - db: &'db dyn Db, - callable_type: Type<'db>, - ) -> Option> { + pub fn new(db: &'db dyn Db, callable_type: Type<'db>) -> Option> { match callable_type { Type::FunctionLiteral(function) => Some(CallableDescription { kind: "function", @@ -4791,7 +4773,7 @@ impl<'db> CallableDescription<'db> { /// Information needed to emit a diagnostic regarding a parameter. #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct ParameterContext { +pub struct ParameterContext { name: Option, index: usize, @@ -4826,7 +4808,7 @@ impl std::fmt::Display for ParameterContext { } #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct ParameterContexts(Vec); +pub struct ParameterContexts(Vec); impl std::fmt::Display for ParameterContexts { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -4843,7 +4825,7 @@ impl std::fmt::Display for ParameterContexts { } #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) enum BindingError<'db> { +pub enum BindingError<'db> { /// The type of an argument is not assignable to the annotated type of its corresponding /// parameter. InvalidArgumentType { @@ -4908,7 +4890,7 @@ pub(crate) enum BindingError<'db> { } impl BindingError<'_> { - pub(crate) fn maybe_apply_argument_index_offset(mut self, offset: Option) -> Self { + pub fn maybe_apply_argument_index_offset(mut self, offset: Option) -> Self { if let Some(offset) = offset { self.apply_argument_index_offset(offset); } @@ -4921,7 +4903,7 @@ impl BindingError<'_> { /// sub-call for a `ParamSpec`, where the argument indices are relative to the sub-call's /// argument list rather than the original call's argument list. The `offset` should be the /// number of arguments in the original call that were matched before the `ParamSpec` component. - pub(crate) fn apply_argument_index_offset(&mut self, offset: usize) { + pub fn apply_argument_index_offset(&mut self, offset: usize) { match self { BindingError::InvalidArgumentType { argument_index, .. } | BindingError::InvalidKeyType { argument_index, .. } @@ -4955,7 +4937,7 @@ impl BindingError<'_> { /// The target of an invalid `@dataclass` application. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum InvalidDataclassTarget { +pub enum InvalidDataclassTarget { NamedTuple, TypedDict, Enum, diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index 93e787e3395c9..ae85789e26404 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -20,27 +20,24 @@ use crate::{ impl<'db> Type<'db> { /// Create a callable type with a single non-overloaded signature. - pub(crate) fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + pub fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { Type::Callable(CallableType::single(db, signature)) } /// Create a non-overloaded, function-like callable type with a single signature. /// /// A function-like callable will bind `self` when accessed as an attribute on an instance. - pub(crate) fn function_like_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + pub fn function_like_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { Type::Callable(CallableType::function_like(db, signature)) } /// Create a non-overloaded callable type which represents the value bound to a `ParamSpec` /// type variable. - pub(crate) fn paramspec_value_callable( - db: &'db dyn Db, - parameters: Parameters<'db>, - ) -> Type<'db> { + pub fn paramspec_value_callable(db: &'db dyn Db, parameters: Parameters<'db>) -> Type<'db> { Type::Callable(CallableType::paramspec_value(db, parameters)) } - pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { + pub fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { match self { Type::Callable(callable) => Some(CallableTypes::one(callable)), @@ -229,12 +226,12 @@ pub enum CallableTypeKind { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct CallableType<'db> { #[returns(ref)] - pub(crate) signatures: CallableSignature<'db>, + pub signatures: CallableSignature<'db>, - pub(super) kind: CallableTypeKind, + pub kind: CallableTypeKind, } -pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, ty: CallableType<'db>, visitor: &V, @@ -248,7 +245,7 @@ pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for CallableType<'_> {} impl<'db> CallableType<'db> { - pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { + pub fn single(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { CallableType::new( db, CallableSignature::single(signature), @@ -256,7 +253,7 @@ impl<'db> CallableType<'db> { ) } - pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { + pub fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { CallableType::new( db, CallableSignature::single(signature), @@ -264,10 +261,7 @@ impl<'db> CallableType<'db> { ) } - pub(crate) fn paramspec_value( - db: &'db dyn Db, - parameters: Parameters<'db>, - ) -> CallableType<'db> { + pub fn paramspec_value(db: &'db dyn Db, parameters: Parameters<'db>) -> CallableType<'db> { CallableType::new( db, CallableSignature::single(Signature::new(parameters, Type::unknown())), @@ -276,27 +270,23 @@ impl<'db> CallableType<'db> { } /// Create a callable type which accepts any parameters and returns an `Unknown` type. - pub(crate) fn unknown(db: &'db dyn Db) -> CallableType<'db> { + pub fn unknown(db: &'db dyn Db) -> CallableType<'db> { Self::single(db, Signature::unknown()) } - pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool { + pub fn is_function_like(self, db: &'db dyn Db) -> bool { matches!(self.kind(db), CallableTypeKind::FunctionLike) } - pub(crate) fn is_classmethod_like(self, db: &'db dyn Db) -> bool { + pub fn is_classmethod_like(self, db: &'db dyn Db) -> bool { matches!(self.kind(db), CallableTypeKind::ClassMethodLike) } - pub(crate) fn is_staticmethod_like(self, db: &'db dyn Db) -> bool { + pub fn is_staticmethod_like(self, db: &'db dyn Db) -> bool { matches!(self.kind(db), CallableTypeKind::StaticMethodLike) } - pub(crate) fn bind_self( - self, - db: &'db dyn Db, - self_type: Option>, - ) -> CallableType<'db> { + pub fn bind_self(self, db: &'db dyn Db, self_type: Option>) -> CallableType<'db> { CallableType::new( db, self.signatures(db).bind_self(db, self_type), @@ -304,7 +294,7 @@ impl<'db> CallableType<'db> { ) } - pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> { + pub fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> { CallableType::new( db, self.signatures(db).apply_self(db, self_type), @@ -316,11 +306,11 @@ impl<'db> CallableType<'db> { /// /// Specifically, this represents a callable type with a single signature: /// `(*args: object, **kwargs: object) -> Never`. - pub(crate) fn bottom(db: &'db dyn Db) -> CallableType<'db> { + pub fn bottom(db: &'db dyn Db) -> CallableType<'db> { Self::new(db, CallableSignature::bottom(), CallableTypeKind::Regular) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -334,7 +324,7 @@ impl<'db> CallableType<'db> { )) } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -353,7 +343,7 @@ impl<'db> CallableType<'db> { ) } - pub(super) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -368,7 +358,7 @@ impl<'db> CallableType<'db> { /// /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -402,40 +392,40 @@ impl<'db> CallableType<'db> { /// Note that this type is guaranteed to contain at least one callable. If you need to support "no /// callables" as a possibility, use `Option`. #[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update)] -pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>); +pub struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>); impl<'db> CallableTypes<'db> { - pub(super) fn new(callables: SmallVec<[CallableType<'db>; 1]>) -> Self { + pub fn new(callables: SmallVec<[CallableType<'db>; 1]>) -> Self { assert!(!callables.is_empty(), "CallableTypes should not be empty"); CallableTypes(callables) } - pub(crate) fn one(callable: CallableType<'db>) -> Self { + pub fn one(callable: CallableType<'db>) -> Self { CallableTypes(smallvec_inline![callable]) } - pub(crate) fn from_elements(callables: impl IntoIterator>) -> Self { + pub fn from_elements(callables: impl IntoIterator>) -> Self { let callables: SmallVec<_> = callables.into_iter().collect(); assert!(!callables.is_empty(), "CallableTypes should not be empty"); CallableTypes(callables) } - pub(crate) fn exactly_one(self) -> Option> { + pub fn exactly_one(self) -> Option> { match self.0.as_slice() { [single] => Some(*single), _ => None, } } - pub(super) fn as_slice(&self) -> &[CallableType<'db>] { + pub fn as_slice(&self) -> &[CallableType<'db>] { &self.0 } - pub(super) fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { + pub fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { self.0 } - pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn into_type(self, db: &'db dyn Db) -> Type<'db> { match self.0.as_slice() { [] => unreachable!("CallableTypes should not be empty"), [single] => Type::Callable(*single), @@ -443,12 +433,12 @@ impl<'db> CallableTypes<'db> { } } - pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self { + pub fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self { Self::from_elements(self.0.iter().map(|element| f(*element))) } #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: CallableType<'db>, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index bc74889549eff..2f88864ab7ce2 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1,14 +1,14 @@ use std::fmt::Write; -pub(crate) use self::dynamic_literal::{ +pub use self::dynamic_literal::{ DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, }; pub use self::known::KnownClass; use self::named_tuple::synthesize_namedtuple_class_member; -pub(super) use self::named_tuple::{ +pub use self::named_tuple::{ DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, NamedTupleField, NamedTupleSpec, }; -pub(crate) use self::static_literal::StaticClassLiteral; +pub use self::static_literal::StaticClassLiteral; use super::{ BoundTypeVarInstance, MemberLookupPolicy, MroIterator, SpecialFormType, SubclassOfType, Type, TypeQualifiers, class_base::ClassBase, function::FunctionType, @@ -56,7 +56,7 @@ mod static_literal; /// A category of classes with code generation capabilities (with synthesized methods). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) enum CodeGeneratorKind<'db> { +pub enum CodeGeneratorKind<'db> { /// Classes decorated with `@dataclass` or similar dataclass-like decorators DataclassLike(Option>), /// Classes inheriting from `typing.NamedTuple` @@ -66,7 +66,7 @@ pub(crate) enum CodeGeneratorKind<'db> { } impl<'db> CodeGeneratorKind<'db> { - pub(crate) fn from_class( + pub fn from_class( db: &'db dyn Db, class: ClassLiteral<'db>, specialization: Option>, @@ -156,7 +156,7 @@ impl<'db> CodeGeneratorKind<'db> { code_generator_of_dynamic_class(db, class) } - pub(super) fn matches( + pub fn matches( self, db: &'db dyn Db, class: ClassLiteral<'db>, @@ -173,7 +173,7 @@ impl<'db> CodeGeneratorKind<'db> { ) } - pub(super) fn dataclass_transformer_params(self) -> Option> { + pub fn dataclass_transformer_params(self) -> Option> { match self { Self::DataclassLike(params) => params, Self::NamedTuple | Self::TypedDict => None, @@ -184,11 +184,11 @@ impl<'db> CodeGeneratorKind<'db> { /// A specialization of a generic class with a particular assignment of types to typevars. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct GenericAlias<'db> { - pub(crate) origin: StaticClassLiteral<'db>, - pub(crate) specialization: Specialization<'db>, + pub origin: StaticClassLiteral<'db>, + pub specialization: Specialization<'db>, } -pub(super) fn walk_generic_alias<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_generic_alias<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, alias: GenericAlias<'db>, visitor: &V, @@ -200,7 +200,7 @@ pub(super) fn walk_generic_alias<'db, V: super::visitor::TypeVisitor<'db> + ?Siz impl get_size2::GetSize for GenericAlias<'_> {} impl<'db> GenericAlias<'db> { - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -214,11 +214,11 @@ impl<'db> GenericAlias<'db> { )) } - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + pub fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.origin(db).definition(db) } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -239,7 +239,7 @@ impl<'db> GenericAlias<'db> { ) } - pub(super) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -250,7 +250,7 @@ impl<'db> GenericAlias<'db> { .find_legacy_typevars_impl(db, binding_context, typevars, visitor); } - pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool { + pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { self.origin(db).is_typed_dict(db) } } @@ -323,7 +323,7 @@ pub enum ClassLiteral<'db> { impl<'db> ClassLiteral<'db> { /// Return a `ClassLiteral` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { + pub fn object(db: &'db dyn Db) -> Self { KnownClass::Object .to_class_literal(db) .as_class_literal() @@ -331,7 +331,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the name of the class. - pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + pub fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { match self { Self::Static(class) => class.name(db), Self::Dynamic(class) => class.name(db), @@ -340,23 +340,23 @@ impl<'db> ClassLiteral<'db> { } /// Returns the known class, if any. - pub(crate) fn known(self, db: &'db dyn Db) -> Option { + pub fn known(self, db: &'db dyn Db) -> Option { self.as_static()?.known(db) } /// Returns whether this class has PEP 695 type parameters. - pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + pub fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { self.as_static() .is_some_and(|class| class.has_pep_695_type_params(db)) } /// Returns an iterator over the MRO. - pub(crate) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { + pub fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { MroIterator::new(db, self, None) } /// Returns the metaclass of this class. - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + pub fn metaclass(self, db: &'db dyn Db) -> Type<'db> { match self { Self::Static(class) => class.metaclass(db), Self::Dynamic(class) => class.metaclass(db), @@ -365,7 +365,7 @@ impl<'db> ClassLiteral<'db> { } /// Look up a class-level member by iterating through the MRO. - pub(crate) fn class_member( + pub fn class_member( self, db: &'db dyn Db, name: &str, @@ -381,7 +381,7 @@ impl<'db> ClassLiteral<'db> { /// Look up a class-level member using a provided MRO iterator. /// /// This is used by `super()` to start the MRO lookup after the pivot class. - pub(super) fn class_member_from_mro( + pub fn class_member_from_mro( self, db: &'db dyn Db, name: &str, @@ -405,7 +405,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns whether this is a known class. - pub(crate) fn is_known(self, db: &'db dyn Db, known: KnownClass) -> bool { + pub fn is_known(self, db: &'db dyn Db, known: KnownClass) -> bool { self.known(db) == Some(known) } @@ -413,7 +413,7 @@ impl<'db> ClassLiteral<'db> { /// /// For static classes, this applies default type arguments. /// For dynamic classes, this returns a non-generic class type. - pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { match self { Self::Static(class) => class.default_specialization(db), Self::Dynamic(_) | Self::DynamicNamedTuple(_) => ClassType::NonGeneric(self), @@ -421,7 +421,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the identity specialization for this class (same as default for non-generic). - pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { match self { Self::Static(class) => class.identity_specialization(db), Self::Dynamic(_) | Self::DynamicNamedTuple(_) => ClassType::NonGeneric(self), @@ -429,12 +429,12 @@ impl<'db> ClassLiteral<'db> { } /// Returns the generic context if this is a generic class. - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + pub fn generic_context(self, db: &'db dyn Db) -> Option> { self.as_static().and_then(|class| class.generic_context(db)) } /// Returns whether this class is a protocol. - pub(crate) fn is_protocol(self, db: &'db dyn Db) -> bool { + pub fn is_protocol(self, db: &'db dyn Db) -> bool { self.as_static().is_some_and(|class| class.is_protocol(db)) } @@ -447,7 +447,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns whether this class is `builtins.tuple` exactly - pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { + pub fn is_tuple(self, db: &'db dyn Db) -> bool { match self { Self::Static(class) => class.is_tuple(db), Self::Dynamic(_) | Self::DynamicNamedTuple(_) => false, @@ -455,20 +455,20 @@ impl<'db> ClassLiteral<'db> { } /// Return a type representing "the set of all instances of the metaclass of this class". - pub(crate) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { self.metaclass(db) .to_instance(db) .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") } /// Returns whether this class is type-check only. - pub(crate) fn type_check_only(self, db: &'db dyn Db) -> bool { + pub fn type_check_only(self, db: &'db dyn Db) -> bool { self.as_static() .is_some_and(|class| class.type_check_only(db)) } /// Returns the file containing the class definition. - pub(crate) fn file(self, db: &dyn Db) -> File { + pub fn file(self, db: &dyn Db) -> File { match self { Self::Static(class) => class.file(db), Self::Dynamic(class) => class.scope(db).file(db), @@ -480,7 +480,7 @@ impl<'db> ClassLiteral<'db> { /// /// For static classes, this is the class name and any arguments passed to the `class` statement. /// For dynamic classes, this is the entire `type()` call expression. - pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + pub fn header_range(self, db: &'db dyn Db) -> TextRange { match self { Self::Static(class) => class.header_range(db), Self::Dynamic(class) => class.header_range(db), @@ -489,12 +489,12 @@ impl<'db> ClassLiteral<'db> { } /// Returns the deprecated info if this class is deprecated. - pub(crate) fn deprecated(self, db: &'db dyn Db) -> Option> { + pub fn deprecated(self, db: &'db dyn Db) -> Option> { self.as_static().and_then(|class| class.deprecated(db)) } /// Returns whether this class is final. - pub(crate) fn is_final(self, db: &'db dyn Db) -> bool { + pub fn is_final(self, db: &'db dyn Db) -> bool { match self { Self::Static(class) => class.is_final(db), // Dynamic classes created via `type()`, `collections.namedtuple()`, etc. cannot be @@ -513,7 +513,7 @@ impl<'db> ClassLiteral<'db> { /// ```python /// X = type("X", (), {"__lt__": lambda self, other: True}) /// ``` - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + pub fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { match self { Self::Static(class) => class.has_own_ordering_method(db), Self::Dynamic(class) => class.has_own_ordering_method(db), @@ -522,7 +522,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the static class definition if this is one. - pub(crate) fn as_static(self) -> Option> { + pub fn as_static(self) -> Option> { match self { Self::Static(class) => Some(class), Self::Dynamic(_) | Self::DynamicNamedTuple(_) => None, @@ -530,7 +530,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the definition of this class, if available. - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + pub fn definition(self, db: &'db dyn Db) -> Option> { match self { Self::Static(class) => Some(class.definition(db)), Self::Dynamic(class) => class.definition(db), @@ -542,7 +542,7 @@ impl<'db> ClassLiteral<'db> { /// /// For static classes, returns `TypeDefinition::StaticClass`. /// For dynamic classes, returns `TypeDefinition::DynamicClass` if a definition is available. - pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option> { + pub fn type_definition(self, db: &'db dyn Db) -> Option> { match self { Self::Static(class) => Some(TypeDefinition::StaticClass(class.definition(db))), Self::Dynamic(class) => class.definition(db).map(TypeDefinition::DynamicClass), @@ -553,7 +553,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the qualified name of this class. - pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { + pub fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { QualifiedClassName::from_class_literal(db, self) } @@ -561,7 +561,7 @@ impl<'db> ClassLiteral<'db> { /// /// For static classes, this is the class header (name and arguments). /// For dynamic classes, this is the `type()` call expression. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + pub fn header_span(self, db: &'db dyn Db) -> Span { match self { Self::Static(class) => class.header_span(db), Self::Dynamic(class) => class.header_span(db), @@ -586,7 +586,7 @@ impl<'db> ClassLiteral<'db> { /// class Foo(int, X): ... /// TypeError: multiple bases have instance lay-out conflict /// ``` - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + pub fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { match self { Self::Static(class) => class.as_disjoint_base(db), Self::Dynamic(class) => class.as_disjoint_base(db), @@ -597,7 +597,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns a non-generic instance of this class. - pub(crate) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { match self { Self::Static(class) => class.to_non_generic_instance(db), Self::Dynamic(_) | Self::DynamicNamedTuple(_) => { @@ -607,7 +607,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the protocol class if this is a protocol. - pub(super) fn into_protocol_class( + pub fn into_protocol_class( self, db: &'db dyn Db, ) -> Option> { @@ -616,7 +616,7 @@ impl<'db> ClassLiteral<'db> { } /// Apply a specialization to this class. - pub(crate) fn apply_specialization( + pub fn apply_specialization( self, db: &'db dyn Db, f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, @@ -628,7 +628,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the instance member lookup. - pub(crate) fn instance_member( + pub fn instance_member( self, db: &'db dyn Db, specialization: Option>, @@ -642,7 +642,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the top materialization for this class. - pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { match self { Self::Static(class) => class.top_materialization(db), Self::Dynamic(_) | Self::DynamicNamedTuple(_) => ClassType::NonGeneric(self), @@ -650,7 +650,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns the `TypedDict` member lookup. - pub(crate) fn typed_dict_member( + pub fn typed_dict_member( self, db: &'db dyn Db, specialization: Option>, @@ -664,7 +664,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns a new `ClassLiteral` with the given dataclass params, preserving all other fields. - pub(crate) fn with_dataclass_params( + pub fn with_dataclass_params( self, db: &'db dyn Db, dataclass_params: Option>, @@ -682,7 +682,7 @@ impl<'db> ClassLiteral<'db> { /// /// Note that when this is a namedtuple this always returns a sequence /// of length one corresponding to `tuple`. - pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + pub fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { match self { Self::Static(static_class) => static_class.explicit_bases(db).into(), Self::Dynamic(dynamic_class) => dynamic_class.explicit_bases(db).into(), @@ -729,22 +729,22 @@ pub enum ClassType<'db> { #[salsa::tracked] impl<'db> ClassType<'db> { /// Return a `ClassType` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { + pub fn object(db: &'db dyn Db) -> Self { ClassType::NonGeneric(ClassLiteral::object(db)) } - pub(super) const fn is_generic(self) -> bool { + pub const fn is_generic(self) -> bool { matches!(self, Self::Generic(_)) } - pub(super) const fn into_generic_alias(self) -> Option> { + pub const fn into_generic_alias(self) -> Option> { match self { Self::NonGeneric(_) => None, Self::Generic(generic) => Some(generic), } } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -758,7 +758,7 @@ impl<'db> ClassType<'db> { } } - pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + pub fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { self.class_literal(db).has_pep_695_type_params(db) } @@ -766,7 +766,7 @@ impl<'db> ClassType<'db> { /// /// For a non-generic class, this returns the class literal directly. /// For a generic alias, this returns the alias's origin. - pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteral<'db> { + pub fn class_literal(self, db: &'db dyn Db) -> ClassLiteral<'db> { match self { Self::NonGeneric(literal) => literal, Self::Generic(generic) => ClassLiteral::Static(generic.origin(db)), @@ -777,7 +777,7 @@ impl<'db> ClassType<'db> { /// /// For a non-generic class, this returns the class literal directly. /// For a generic alias, this returns the alias's origin. - pub(crate) fn class_literal_and_specialization( + pub fn class_literal_and_specialization( self, db: &'db dyn Db, ) -> (ClassLiteral<'db>, Option>) { @@ -792,7 +792,7 @@ impl<'db> ClassType<'db> { /// Returns the statement-defined class literal and specialization for this class. /// For a non-generic class, this is the class itself. For a generic alias, this is the alias's origin. - pub(crate) fn static_class_literal( + pub fn static_class_literal( self, db: &'db dyn Db, ) -> Option<(StaticClassLiteral<'db>, Option>)> { @@ -805,7 +805,7 @@ impl<'db> ClassType<'db> { /// Returns the statement-defined class literal and specialization for this class, with an additional /// specialization applied if the class is generic. - pub(crate) fn static_class_literal_specialized( + pub fn static_class_literal_specialized( self, db: &'db dyn Db, additional_specialization: Option>, @@ -824,44 +824,44 @@ impl<'db> ClassType<'db> { } } - pub(crate) fn name(self, db: &'db dyn Db) -> &'db Name { + pub fn name(self, db: &'db dyn Db) -> &'db Name { self.class_literal(db).name(db) } - pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { + pub fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { self.class_literal(db).qualified_name(db) } - pub(crate) fn known(self, db: &'db dyn Db) -> Option { + pub fn known(self, db: &'db dyn Db) -> Option { self.class_literal(db).known(db) } /// Returns the definition for this class, if available. - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + pub fn definition(self, db: &'db dyn Db) -> Option> { self.class_literal(db).definition(db) } /// Returns the type definition for this class. - pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option> { + pub fn type_definition(self, db: &'db dyn Db) -> Option> { self.class_literal(db).type_definition(db) } /// Return `Some` if this class is known to be a [`DisjointBase`], or `None` if it is not. - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + pub fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { self.class_literal(db).as_disjoint_base(db) } /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { + pub fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { self.known(db) == Some(known_class) } /// Return `true` if this class represents the builtin class `object` - pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { + pub fn is_object(self, db: &'db dyn Db) -> bool { self.is_known(db, KnownClass::Object) } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -876,7 +876,7 @@ impl<'db> ClassType<'db> { } } - pub(super) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -899,7 +899,7 @@ impl<'db> ClassType<'db> { /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(super) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { + pub fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { match self { Self::NonGeneric(class) => class.iter_mro(db), Self::Generic(generic) => MroIterator::new( @@ -912,7 +912,7 @@ impl<'db> ClassType<'db> { /// Iterate over the method resolution order ("MRO") of the class, optionally applying an /// additional specialization to it if the class is generic. - pub(super) fn iter_mro_specialized( + pub fn iter_mro_specialized( self, db: &'db dyn Db, additional_specialization: Option>, @@ -932,7 +932,7 @@ impl<'db> ClassType<'db> { } /// Is this class final? - pub(super) fn is_final(self, db: &'db dyn Db) -> bool { + pub fn is_final(self, db: &'db dyn Db) -> bool { self.class_literal(db).is_final(db) } @@ -941,7 +941,7 @@ impl<'db> ClassType<'db> { /// /// The value of the map is a struct containing information about the abstract method. #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn abstract_methods(self, db: &'db dyn Db) -> FxIndexMap> { + pub fn abstract_methods(self, db: &'db dyn Db) -> FxIndexMap> { fn type_as_abstract_method<'db>( db: &'db dyn Db, ty: Type<'db>, @@ -1036,7 +1036,7 @@ impl<'db> ClassType<'db> { /// Returns `true` if any class in this class's MRO (excluding `object`) defines an ordering /// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation. - pub(super) fn has_ordering_method_in_mro(self, db: &'db dyn Db) -> bool { + pub fn has_ordering_method_in_mro(self, db: &'db dyn Db) -> bool { self.iter_mro(db) .filter_map(ClassBase::into_class) .filter(|class| !class.is_object(db)) @@ -1044,7 +1044,7 @@ impl<'db> ClassType<'db> { } /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + pub fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { self.when_subclass_of( db, other, @@ -1054,7 +1054,7 @@ impl<'db> ClassType<'db> { .is_always_satisfied(db) } - pub(super) fn when_subclass_of<'c>( + pub fn when_subclass_of<'c>( self, db: &'db dyn Db, other: ClassType<'db>, @@ -1073,7 +1073,7 @@ impl<'db> ClassType<'db> { } #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -1134,7 +1134,7 @@ impl<'db> ClassType<'db> { } /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. - pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + pub fn metaclass(self, db: &'db dyn Db) -> Type<'db> { match self { Self::NonGeneric(class) => class.metaclass(db), Self::Generic(generic) => generic @@ -1148,14 +1148,14 @@ impl<'db> ClassType<'db> { /// /// Returns `None` if this class does not have any disjoint bases in its MRO. #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - pub(super) fn nearest_disjoint_base(self, db: &'db dyn Db) -> Option> { + pub fn nearest_disjoint_base(self, db: &'db dyn Db) -> Option> { self.iter_mro(db) .filter_map(ClassBase::into_class) .find_map(|base| base.as_disjoint_base(db)) } /// Return `true` if this class could exist in the MRO of `other`. - pub(super) fn could_exist_in_mro_of( + pub fn could_exist_in_mro_of( self, db: &'db dyn Db, other: Self, @@ -1190,7 +1190,7 @@ impl<'db> ClassType<'db> { /// For two given classes `A` and `B`, it is often possible to say for sure /// that there could never exist any class `C` that inherits from both `A` and `B`. /// In these situations, this method returns `false`; in all others, it returns `true`. - pub(super) fn could_coexist_in_mro_with( + pub fn could_coexist_in_mro_with( self, db: &'db dyn Db, other: Self, @@ -1258,7 +1258,7 @@ impl<'db> ClassType<'db> { } /// Return a type representing "the set of all instances of the metaclass of this class". - pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { self .metaclass(db) .to_instance(db) @@ -1270,7 +1270,7 @@ impl<'db> ClassType<'db> { /// The member resolves to a member on the class itself or any of its proper superclasses. /// /// TODO: Should this be made private...? - pub(super) fn class_member( + pub fn class_member( self, db: &'db dyn Db, name: &str, @@ -1298,7 +1298,7 @@ impl<'db> ClassType<'db> { /// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope /// directly. Use [`ClassType::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. - pub(super) fn own_class_member( + pub fn own_class_member( self, db: &'db dyn Db, inherited_generic_context: Option>, @@ -1600,7 +1600,7 @@ impl<'db> ClassType<'db> { /// Look up an instance attribute (available in `__dict__`) of the given name. /// /// See [`Type::instance_member`] for more details. - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { match self { Self::NonGeneric(ClassLiteral::Dynamic(class)) => class.instance_member(db, name), Self::NonGeneric(ClassLiteral::DynamicNamedTuple(namedtuple)) => { @@ -1629,7 +1629,7 @@ impl<'db> ClassType<'db> { /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { match self { Self::NonGeneric(ClassLiteral::Dynamic(dynamic)) => { dynamic.own_instance_member(db, name) @@ -1653,7 +1653,7 @@ impl<'db> ClassType<'db> { /// Return a callable type (or union of callable types) that represents the callable /// constructor signature of this class. #[salsa::tracked(cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { + pub fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { // TODO: This mimics a lot of the logic in Type::try_call_from_constructor. Can we // consolidate the two? Can we invoke a class by upcasting the class into a Callable, and // then relying on the call binding machinery to Just Work™? @@ -1840,7 +1840,7 @@ impl<'db> ClassType<'db> { } } - pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { + pub fn is_protocol(self, db: &'db dyn Db) -> bool { self.static_class_literal(db) .is_some_and(|(class, _)| class.is_protocol(db)) } @@ -1849,7 +1849,7 @@ impl<'db> ClassType<'db> { /// /// For static classes, this is the class header (name and arguments). /// For dynamic classes, this is the `type()` call expression. - pub(super) fn definition_span(self, db: &'db dyn Db) -> Span { + pub fn definition_span(self, db: &'db dyn Db) -> Span { self.class_literal(db).header_span(db) } } @@ -1908,23 +1908,23 @@ impl<'db> VarianceInferable<'db> for ClassType<'db> { } #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)] -pub(super) struct AbstractMethod<'db> { - pub(super) defining_class: ClassType<'db>, - pub(super) definition: Definition<'db>, - pub(super) kind: AbstractMethodKind, +pub struct AbstractMethod<'db> { + pub defining_class: ClassType<'db>, + pub definition: Definition<'db>, + pub kind: AbstractMethodKind, } /// A filter that describes which methods are considered when looking for implicit attribute assignments /// in [`StaticClassLiteral::implicit_attribute`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(super) enum MethodDecorator { +pub enum MethodDecorator { None, ClassMethod, StaticMethod, } impl MethodDecorator { - pub(crate) fn try_from_fn_type(db: &dyn Db, fn_type: FunctionType) -> Result { + pub fn try_from_fn_type(db: &dyn Db, fn_type: FunctionType) -> Result { match (fn_type.is_classmethod(db), fn_type.is_staticmethod(db)) { (true, true) => Err(()), // A method can't be static and class method at the same time. (true, false) => Ok(Self::ClassMethod), @@ -1933,7 +1933,7 @@ impl MethodDecorator { } } - pub(crate) const fn description(self) -> &'static str { + pub const fn description(self) -> &'static str { match self { MethodDecorator::None => "an instance method", MethodDecorator::ClassMethod => "a classmethod", @@ -1944,7 +1944,7 @@ impl MethodDecorator { /// Kind-specific metadata for different types of fields #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) enum FieldKind<'db> { +pub enum FieldKind<'db> { /// `NamedTuple` field metadata NamedTuple { default_ty: Option> }, /// dataclass field metadata @@ -1972,18 +1972,18 @@ pub(crate) enum FieldKind<'db> { /// Metadata regarding a dataclass field/attribute or a `TypedDict` "item" / key-value pair. #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct Field<'db> { +pub struct Field<'db> { /// The declared type of the field - pub(crate) declared_ty: Type<'db>, + pub declared_ty: Type<'db>, /// Kind-specific metadata for this field - pub(crate) kind: FieldKind<'db>, + pub kind: FieldKind<'db>, /// The first declaration of this field. /// This field is used for backreferences in diagnostics. - pub(crate) first_declaration: Option>, + pub first_declaration: Option>, } impl Field<'_> { - pub(crate) const fn is_required(&self) -> bool { + pub const fn is_required(&self) -> bool { match &self.kind { FieldKind::NamedTuple { default_ty } => default_ty.is_none(), // A dataclass field is NOT required if `default` (or `default_factory`) is set @@ -1995,7 +1995,7 @@ impl Field<'_> { } } - pub(crate) const fn is_read_only(&self) -> bool { + pub const fn is_read_only(&self) -> bool { match &self.kind { FieldKind::TypedDict { is_read_only, .. } => *is_read_only, _ => false, @@ -2006,7 +2006,7 @@ impl Field<'_> { impl<'db> Field<'db> { /// Returns true if this field is a `dataclasses.KW_ONLY` sentinel. /// - pub(crate) fn is_kw_only_sentinel(&self, db: &'db dyn Db) -> bool { + pub fn is_kw_only_sentinel(&self, db: &'db dyn Db) -> bool { self.declared_ty.is_instance_of(db, KnownClass::KwOnly) } } @@ -2025,14 +2025,14 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// This struct encapsulates the shared logic for looking up class and instance /// members by iterating through an MRO. Both `StaticClassLiteral` and `DynamicClassLiteral` /// use this to avoid duplicating the MRO traversal logic. -pub(super) struct MroLookup<'db, I> { +pub struct MroLookup<'db, I> { db: &'db dyn Db, mro_iter: I, } impl<'db, I: Iterator>> MroLookup<'db, I> { /// Create a new MRO lookup from a database and an MRO iterator. - pub(super) fn new(db: &'db dyn Db, mro_iter: I) -> Self { + pub fn new(db: &'db dyn Db, mro_iter: I) -> Self { Self { db, mro_iter } } @@ -2050,7 +2050,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { /// If we encounter a dynamic type in the MRO, we save it and after traversal: /// 1. Use it as the type if no other classes define the attribute, or /// 2. Intersect it with the type from non-dynamic MRO members. - pub(super) fn class_member( + pub fn class_member( self, name: &str, policy: MemberLookupPolicy, @@ -2127,7 +2127,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { /// /// Returns `InstanceMemberResult::TypedDict` if a `TypedDict` base is encountered, /// allowing the caller to handle this case specially. - pub(super) fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { + pub fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { let db = self.db; let mut union = UnionBuilder::new(db); let mut union_qualifiers = TypeQualifiers::empty(); @@ -2205,21 +2205,21 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { } /// Result of class member lookup from MRO iteration. -pub(super) enum ClassMemberResult<'db> { +pub enum ClassMemberResult<'db> { /// Found the member or exhausted the MRO. Done(CompletedMemberLookup<'db>), /// Encountered a `TypedDict` base. TypedDict, } -pub(super) struct CompletedMemberLookup<'db> { +pub struct CompletedMemberLookup<'db> { lookup_result: LookupResult<'db>, dynamic_type: Option>, } impl<'db> CompletedMemberLookup<'db> { /// Finalize the lookup result by handling dynamic type intersection. - pub(super) fn finalize(self, db: &'db dyn Db) -> PlaceAndQualifiers<'db> { + pub fn finalize(self, db: &'db dyn Db) -> PlaceAndQualifiers<'db> { match ( PlaceAndQualifiers::from(self.lookup_result), self.dynamic_type, @@ -2253,7 +2253,7 @@ impl<'db> CompletedMemberLookup<'db> { /// Result of instance member lookup from MRO iteration. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) enum InstanceMemberResult<'db> { +pub enum InstanceMemberResult<'db> { /// Found the member or exhausted the MRO Done(PlaceAndQualifiers<'db>), /// Encountered a `TypedDict` base - caller should handle this specially @@ -2265,13 +2265,13 @@ pub(super) enum InstanceMemberResult<'db> { // have the same components. You'd expect them to compare equal, but they'd compare // unequal if `PartialEq`/`Eq` were naively derived. #[derive(Clone, Copy)] -pub(super) struct QualifiedClassName<'db> { +pub struct QualifiedClassName<'db> { db: &'db dyn Db, class: ClassLiteral<'db>, } impl<'db> QualifiedClassName<'db> { - pub(super) fn from_class_literal(db: &'db dyn Db, class: ClassLiteral<'db>) -> Self { + pub fn from_class_literal(db: &'db dyn Db, class: ClassLiteral<'db>) -> Self { Self { db, class } } @@ -2281,7 +2281,7 @@ impl<'db> QualifiedClassName<'db> { /// `["a", "b"]`. Calling this method on a class `D` inside the namespace of a method /// `m` inside the namespace of a class `C` in the module `a.b` would return /// `["a", "b", "C", ""]`. - pub(super) fn components_excluding_self(&self) -> Vec { + pub fn components_excluding_self(&self) -> Vec { let (file, file_scope_id, skip_count) = match self.class { ClassLiteral::Static(class) => { let body_scope = class.body_scope(self.db); @@ -2336,9 +2336,9 @@ impl std::fmt::Display for QualifiedClassName<'_> { /// /// [PEP 800]: https://peps.python.org/pep-0800/ #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, get_size2::GetSize, salsa::Update)] -pub(super) struct DisjointBase<'db> { - pub(super) class: ClassLiteral<'db>, - pub(super) kind: DisjointBaseKind, +pub struct DisjointBase<'db> { + pub class: ClassLiteral<'db>, + pub kind: DisjointBaseKind, } impl<'db> DisjointBase<'db> { @@ -2375,7 +2375,7 @@ impl<'db> DisjointBase<'db> { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)] -pub(super) enum DisjointBaseKind { +pub enum DisjointBaseKind { /// We know the class is a disjoint base because it's either hardcoded in ty /// or has the `@disjoint_base` decorator. DisjointBaseDecorator, @@ -2384,19 +2384,19 @@ pub(super) enum DisjointBaseKind { } #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(super) struct MetaclassError<'db> { +pub struct MetaclassError<'db> { kind: MetaclassErrorKind<'db>, } impl<'db> MetaclassError<'db> { /// Return an [`MetaclassErrorKind`] variant describing why we could not resolve the metaclass for this class. - pub(super) fn reason(&self) -> &MetaclassErrorKind<'db> { + pub fn reason(&self) -> &MetaclassErrorKind<'db> { &self.kind } } #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(super) enum MetaclassErrorKind<'db> { +pub enum MetaclassErrorKind<'db> { /// The class has incompatible metaclasses in its inheritance hierarchy. /// /// The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all diff --git a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs index 6db6793f7e37d..4a946381e3d4b 100644 --- a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs +++ b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs @@ -111,7 +111,7 @@ impl get_size2::GetSize for DynamicClassLiteral<'_> {} #[salsa::tracked] impl<'db> DynamicClassLiteral<'db> { /// Returns the definition where this class is created, if it was assigned to a variable. - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + pub fn definition(self, db: &'db dyn Db) -> Option> { match self.anchor(db) { DynamicClassAnchor::Definition(definition) => Some(*definition), DynamicClassAnchor::ScopeOffset { .. } => None, @@ -119,7 +119,7 @@ impl<'db> DynamicClassLiteral<'db> { } /// Returns the scope in which this dynamic class was created. - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { match self.anchor(db) { DynamicClassAnchor::Definition(definition) => definition.scope(db), DynamicClassAnchor::ScopeOffset { scope, .. } => *scope, @@ -139,7 +139,7 @@ impl<'db> DynamicClassLiteral<'db> { /// or if the bases argument is not a tuple. /// /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). - pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { + pub fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { /// Inner cached function for deferred inference of bases. /// Only called for assigned `type()` calls where inference was deferred. #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] @@ -185,12 +185,12 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns a [`Span`] with the range of the `type()` call expression. /// /// See [`Self::header_range`] for more details. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + pub fn header_span(self, db: &'db dyn Db) -> Span { Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) } /// Returns the range of the `type()` call expression that created this class. - pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + pub fn header_range(self, db: &'db dyn Db) -> TextRange { let scope = self.scope(db); let file = scope.file(db); let module = parsed_module(db, file).load(db); @@ -229,7 +229,7 @@ impl<'db> DynamicClassLiteral<'db> { /// that is a subclass of all other base metaclasses. /// /// See - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + pub fn metaclass(self, db: &'db dyn Db) -> Type<'db> { self.try_metaclass(db) .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) } @@ -240,7 +240,7 @@ impl<'db> DynamicClassLiteral<'db> { /// (i.e., two base classes have metaclasses that are not in a subclass relationship). /// /// See - pub(crate) fn try_metaclass( + pub fn try_metaclass( self, db: &'db dyn Db, ) -> Result, DynamicMetaclassConflict<'db>> { @@ -322,12 +322,12 @@ impl<'db> DynamicClassLiteral<'db> { /// /// If the MRO cannot be computed (e.g., due to inconsistent ordering), falls back /// to iterating over base MROs sequentially with deduplication. - pub(crate) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { + pub fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { MroIterator::new(db, ClassLiteral::Dynamic(self), None) } /// Look up an instance member by iterating through the MRO. - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { match MroLookup::new(db, self.iter_mro(db)).instance_member(name) { InstanceMemberResult::Done(result) => result, InstanceMemberResult::TypedDict => { @@ -344,7 +344,7 @@ impl<'db> DynamicClassLiteral<'db> { /// Uses `MroLookup` with: /// - No inherited generic context (dynamic classes aren't generic). /// - `is_self_object = false` (dynamic classes are never `object`). - pub(crate) fn class_member( + pub fn class_member( self, db: &'db dyn Db, name: &str, @@ -392,7 +392,7 @@ impl<'db> DynamicClassLiteral<'db> { /// /// Returns [`Member::unbound`] if the member is not found in the namespace dict, /// unless the namespace is dynamic, in which case returns `Unknown`. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { // If the namespace is dynamic (not a literal dict) and the name isn't in `self.members`, // return Unknown since we can't know what attributes might be defined. self.members(db) @@ -407,7 +407,7 @@ impl<'db> DynamicClassLiteral<'db> { /// /// For dynamic classes, instance members are the same as class members /// since they come from the namespace dict. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { self.own_class_member(db, name) } @@ -416,7 +416,7 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns `Ok(Mro)` if successful, or `Err(DynamicMroError)` if there's /// an error (duplicate bases or C3 linearization failure). #[salsa::tracked(returns(ref), cycle_initial=dynamic_class_try_mro_cycle_initial, heap_size = ruff_memory_usage::heap_size)] - pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> { + pub fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> { Mro::of_dynamic_class(db, self) } @@ -427,7 +427,7 @@ impl<'db> DynamicClassLiteral<'db> { /// ```python /// X = type("X", (), {"__slots__": ("a",)}) /// ``` - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + pub fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { // Check if __slots__ is in the members for (name, ty) in self.members(db) { if name.as_str() == "__slots__" { @@ -457,7 +457,7 @@ impl<'db> DynamicClassLiteral<'db> { /// if synthesis is valid. /// /// If the namespace is dynamic, returns `true` since we can't know if ordering methods exist. - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + pub fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { const ORDERING_METHODS: &[&str] = &["__lt__", "__le__", "__gt__", "__ge__"]; ORDERING_METHODS .iter() @@ -465,7 +465,7 @@ impl<'db> DynamicClassLiteral<'db> { } /// Returns a new [`DynamicClassLiteral`] with the given dataclass params, preserving all other fields. - pub(crate) fn with_dataclass_params( + pub fn with_dataclass_params( self, db: &'db dyn Db, dataclass_params: Option>, @@ -485,13 +485,13 @@ impl<'db> DynamicClassLiteral<'db> { /// /// This mirrors `MetaclassErrorKind::Conflict` for regular classes. #[derive(Debug, Clone)] -pub(crate) struct DynamicMetaclassConflict<'db> { +pub struct DynamicMetaclassConflict<'db> { /// The first conflicting metaclass and its originating base class. - pub(crate) metaclass1: ClassType<'db>, - pub(crate) base1: ClassBase<'db>, + pub metaclass1: ClassType<'db>, + pub base1: ClassBase<'db>, /// The second conflicting metaclass and its originating base class. - pub(crate) metaclass2: ClassType<'db>, - pub(crate) base2: ClassBase<'db>, + pub metaclass2: ClassType<'db>, + pub base2: ClassBase<'db>, } #[expect(clippy::unnecessary_wraps)] diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index cc85970a0132a..d2e7ea713a1e7 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -139,11 +139,11 @@ pub enum KnownClass { } impl KnownClass { - pub(crate) const fn is_bool(self) -> bool { + pub const fn is_bool(self) -> bool { matches!(self, Self::Bool) } - pub(crate) const fn is_special_form(self) -> bool { + pub const fn is_special_form(self) -> bool { matches!(self, Self::SpecialForm) } @@ -152,7 +152,7 @@ impl KnownClass { /// /// Returns `None` for `KnownClass::Tuple`, since the truthiness of a tuple /// depends on its spec. - pub(crate) const fn bool(self) -> Option { + pub const fn bool(self) -> Option { match self { // N.B. It's only generally safe to infer `Truthiness::AlwaysTrue` for a `KnownClass` // variant if the class's `__bool__` method always returns the same thing *and* the @@ -254,7 +254,7 @@ impl KnownClass { /// Return `true` if this class is a subclass of `enum.Enum` *and* has enum members, i.e. /// if it is an "actual" enum, not `enum.Enum` itself or a similar custom enum class. - pub(crate) const fn is_enum_subclass_with_members(self) -> bool { + pub const fn is_enum_subclass_with_members(self) -> bool { match self { KnownClass::Bool | KnownClass::Object @@ -343,7 +343,7 @@ impl KnownClass { } /// Return `true` if this class is a (true) subclass of `typing.TypedDict`. - pub(crate) const fn is_typed_dict_subclass(self) -> bool { + pub const fn is_typed_dict_subclass(self) -> bool { match self { KnownClass::Bool | KnownClass::Object @@ -431,7 +431,7 @@ impl KnownClass { } } - pub(crate) const fn is_tuple_subclass(self) -> bool { + pub const fn is_tuple_subclass(self) -> bool { match self { KnownClass::Tuple | KnownClass::VersionInfo => true, @@ -531,7 +531,7 @@ impl KnownClass { /// on, but it causes problems if we attempt to infer the types of their bases /// too soon. /// 2. It's probably more performant. - pub(crate) const fn is_protocol(self) -> bool { + pub const fn is_protocol(self) -> bool { match self { Self::SupportsIndex | Self::Iterable @@ -625,7 +625,7 @@ impl KnownClass { /// classes need special treatment in some places. For example, implicit usages of `Self` should not /// be eagerly replaced with the fallback class itself. Instead, `Self` should eventually be treated /// as referring to the destination type (e.g. the actual `NamedTuple`). - pub(crate) const fn is_fallback_class(self) -> bool { + pub const fn is_fallback_class(self) -> bool { match self { KnownClass::Bool | KnownClass::Object @@ -712,7 +712,7 @@ impl KnownClass { } } - pub(crate) fn name(self, db: &dyn Db) -> &'static str { + pub fn name(self, db: &dyn Db) -> &'static str { match self { Self::Bool => "bool", Self::Object => "object", @@ -828,7 +828,7 @@ impl KnownClass { } } - pub(crate) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { + pub fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { struct KnownClassDisplay<'db> { db: &'db dyn Db, class: KnownClass, @@ -872,7 +872,7 @@ impl KnownClass { /// Similar to [`KnownClass::to_instance`], but returns the Unknown-specialization where each type /// parameter is specialized to `Unknown`. #[track_caller] - pub(crate) fn to_instance_unknown(self, db: &dyn Db) -> Type<'_> { + pub fn to_instance_unknown(self, db: &dyn Db) -> Type<'_> { debug_assert_ne!( self, KnownClass::Tuple, @@ -888,7 +888,7 @@ impl KnownClass { /// /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong /// number of types, a debug-level log message will be emitted stating this. - pub(crate) fn to_specialized_class_type<'t, 'db, T>( + pub fn to_specialized_class_type<'t, 'db, T>( self, db: &'db dyn Db, specialization: T, @@ -942,7 +942,7 @@ impl KnownClass { /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong /// number of types, a debug-level log message will be emitted stating this. #[track_caller] - pub(crate) fn to_specialized_instance<'t, 'db, T>( + pub fn to_specialized_instance<'t, 'db, T>( self, db: &'db dyn Db, specialization: T, @@ -991,7 +991,7 @@ impl KnownClass { /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. /// /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> { + pub fn try_to_class_literal(self, db: &dyn Db) -> Option> { #[salsa::interned(heap_size=ruff_memory_usage::heap_size)] struct KnownClassArgument { class: KnownClass, @@ -1043,7 +1043,7 @@ impl KnownClass { /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. /// /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type<'_> { + pub fn to_class_literal(self, db: &dyn Db) -> Type<'_> { self.try_to_class_literal(db) .map(|class| Type::ClassLiteral(ClassLiteral::Static(class))) .unwrap_or_else(Type::unknown) @@ -1062,12 +1062,12 @@ impl KnownClass { /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, /// *and* `class` is a subclass of `other`. - pub(crate) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + pub fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { self.try_to_class_literal_without_logging(db) .is_ok_and(|class| class.is_subclass_of(db, None, other)) } - pub(crate) fn when_subclass_of<'db, 'c>( + pub fn when_subclass_of<'db, 'c>( self, db: &'db dyn Db, other: ClassType<'db>, @@ -1077,7 +1077,7 @@ impl KnownClass { } /// Return the module in which we should look up the definition for this class - pub(super) fn canonical_module(self, db: &dyn Db) -> KnownModule { + pub fn canonical_module(self, db: &dyn Db) -> KnownModule { match self { Self::Bool | Self::Object @@ -1198,7 +1198,7 @@ impl KnownClass { /// Returns `Some(true)` if all instances of this `KnownClass` compare equal. /// Returns `None` for `KnownClass::Tuple`, since whether or not a tuple type /// is single-valued depends on the tuple spec. - pub(crate) const fn is_single_valued(self) -> Option { + pub const fn is_single_valued(self) -> Option { match self { Self::NoneType | Self::NoDefaultType @@ -1291,7 +1291,7 @@ impl KnownClass { /// Is this class a singleton class? /// /// A singleton class is a class where it is known that only one instance can ever exist at runtime. - pub(crate) const fn is_singleton(self) -> bool { + pub const fn is_singleton(self) -> bool { match self { Self::NoneType | Self::EllipsisType @@ -1380,11 +1380,7 @@ impl KnownClass { } } - pub(crate) fn try_from_file_and_name( - db: &dyn Db, - file: File, - class_name: &str, - ) -> Option { + pub fn try_from_file_and_name(db: &dyn Db, file: File, class_name: &str) -> Option { // We assert that this match is exhaustive over the right-hand side in the unit test // `known_class_roundtrip_from_str()` let candidates: &[Self] = match class_name { @@ -1586,7 +1582,7 @@ impl KnownClass { /// Evaluate a call to this known class, emit any diagnostics that are necessary /// as a result of the call, and return the type that results from the call. - pub(crate) fn check_call<'db>( + pub fn check_call<'db>( self, context: &InferContext<'db, '_>, index: &SemanticIndex<'db>, @@ -1731,7 +1727,7 @@ impl KnownClass { /// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum KnownClassLookupError<'db> { +pub enum KnownClassLookupError<'db> { /// There is no symbol by that name in the expected typeshed module. ClassNotFound, /// There is a symbol by that name in the expected typeshed module, diff --git a/crates/ty_python_semantic/src/types/class/named_tuple.rs b/crates/ty_python_semantic/src/types/class/named_tuple.rs index 3c4b570f8e928..bf6689e46682f 100644 --- a/crates/ty_python_semantic/src/types/class/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/class/named_tuple.rs @@ -22,7 +22,7 @@ use crate::{ /// /// The `inherited_generic_context` parameter is used for declarative namedtuples to preserve /// generic context in the synthesized `__new__` signature. -pub(super) fn synthesize_namedtuple_class_member<'db>( +pub fn synthesize_namedtuple_class_member<'db>( db: &'db dyn Db, name: &str, instance_ty: Type<'db>, @@ -110,9 +110,9 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( #[derive(Debug, salsa::Update, get_size2::GetSize, Clone, PartialEq, Eq, Hash)] pub struct NamedTupleField<'db> { - pub(crate) name: Name, - pub(crate) ty: Type<'db>, - pub(crate) default: Option>, + pub name: Name, + pub ty: Type<'db>, + pub default: Option>, } /// A namedtuple created via the functional form `namedtuple(name, fields)` or @@ -149,7 +149,7 @@ impl get_size2::GetSize for DynamicNamedTupleLiteral<'_> {} #[salsa::tracked] impl<'db> DynamicNamedTupleLiteral<'db> { /// Returns the definition where this namedtuple is created, if it was assigned to a variable. - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + pub fn definition(self, db: &'db dyn Db) -> Option> { match self.anchor(db) { DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } | DynamicNamedTupleAnchor::TypingDefinition(definition) => Some(*definition), @@ -158,7 +158,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Returns the scope in which this dynamic class was created. - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { match self.anchor(db) { DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } | DynamicNamedTupleAnchor::TypingDefinition(definition) => definition.scope(db), @@ -167,12 +167,12 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Returns an instance type for this dynamic namedtuple. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_instance(self, db: &'db dyn Db) -> Type<'db> { Type::instance(db, ClassType::NonGeneric(self.into())) } /// Returns the range of the namedtuple call expression. - pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + pub fn header_range(self, db: &'db dyn Db) -> TextRange { let scope = self.scope(db); let file = scope.file(db); let module = parsed_module(db, file).load(db); @@ -207,7 +207,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Returns a [`Span`] pointing to the namedtuple call expression. - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + pub fn header_span(self, db: &'db dyn Db) -> Span { Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) } @@ -231,7 +231,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { heap_size=ruff_memory_usage::heap_size, cycle_initial=dynamic_namedtuple_mro_cycle_initial )] - pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { + pub fn mro(self, db: &'db dyn Db) -> Mro<'db> { let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); let tuple_class = self.tuple_base_class(db); std::iter::once(self_base) @@ -242,7 +242,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { /// Get the metaclass of this dynamic namedtuple. /// /// Namedtuples always have `type` as their metaclass. - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + pub fn metaclass(self, db: &'db dyn Db) -> Type<'db> { let _ = self; KnownClass::Type.to_class_literal(db) } @@ -250,7 +250,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { /// Compute the specialized tuple class that this namedtuple inherits from. /// /// For example, `namedtuple("Point", [("x", int), ("y", int)])` inherits from `tuple[int, int]`. - pub(crate) fn tuple_base_class(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn tuple_base_class(self, db: &'db dyn Db) -> ClassType<'db> { // If fields are unknown, return `tuple[Unknown, ...]` to avoid false positives // like index-out-of-bounds errors. if !self.has_known_fields(db) { @@ -273,7 +273,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { /// /// For dynamic namedtuples, instance members are the field names. /// If fields are unknown (dynamic), returns `Any` for any attribute. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { for field in self.fields(db) { if field.name == name { return Member::definitely_declared(field.ty); @@ -288,7 +288,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Look up an instance member by name (including superclasses). - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { // First check own instance members. let result = self.own_instance_member(db, name); if !result.is_undefined() { @@ -300,7 +300,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Look up a class-level member by name. - pub(crate) fn class_member( + pub fn class_member( self, db: &'db dyn Db, name: &str, @@ -331,7 +331,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { /// /// This only checks synthesized members and field properties, without falling /// back to tuple or other base classes. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { // Handle synthesized namedtuple attributes. if let Some(ty) = self.synthesized_class_member(db, name) { return Member::definitely_declared(ty); @@ -444,7 +444,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { self.spec(db).fields(db) } - pub(super) fn has_known_fields(self, db: &'db dyn Db) -> bool { + pub fn has_known_fields(self, db: &'db dyn Db) -> bool { self.spec(db).has_known_fields(db) } } @@ -522,23 +522,23 @@ pub enum DynamicNamedTupleAnchor<'db> { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct NamedTupleSpec<'db> { #[returns(deref)] - pub(crate) fields: Box<[NamedTupleField<'db>]>, + pub fields: Box<[NamedTupleField<'db>]>, - pub(crate) has_known_fields: bool, + pub has_known_fields: bool, } impl<'db> NamedTupleSpec<'db> { /// Create a [`NamedTupleSpec`] with the given fields. - pub(crate) fn known(db: &'db dyn Db, fields: Box<[NamedTupleField<'db>]>) -> Self { + pub fn known(db: &'db dyn Db, fields: Box<[NamedTupleField<'db>]>) -> Self { Self::new(db, fields, true) } /// Create a [`NamedTupleSpec`] that indicates a namedtuple class has unknown fields. - pub(crate) fn unknown(db: &'db dyn Db) -> Self { + pub fn unknown(db: &'db dyn Db) -> Self { Self::new(db, Box::default(), false) } - pub(crate) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index 972d3908a5aa5..53e6d1d701b66 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -69,22 +69,22 @@ use crate::{ pub struct StaticClassLiteral<'db> { /// Name of the class at definition #[returns(ref)] - pub(crate) name: Name, + pub name: Name, - pub(crate) body_scope: ScopeId<'db>, + pub body_scope: ScopeId<'db>, - pub(crate) known: Option, + pub known: Option, /// If this class is deprecated, this holds the deprecation message. - pub(crate) deprecated: Option>, + pub deprecated: Option>, - pub(crate) type_check_only: bool, + pub type_check_only: bool, - pub(crate) dataclass_params: Option>, - pub(crate) dataclass_transformer_params: Option>, + pub dataclass_params: Option>, + pub dataclass_transformer_params: Option>, /// Whether this class is decorated with `@functools.total_ordering` - pub(crate) total_ordering: bool, + pub total_ordering: bool, } // The Salsa heap is tracked separately. @@ -101,11 +101,11 @@ fn generic_context_cycle_initial<'db>( #[salsa::tracked] impl<'db> StaticClassLiteral<'db> { /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { + pub fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { self.known(db) == Some(known_class) } - pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { + pub fn is_tuple(self, db: &'db dyn Db) -> bool { self.is_known(db, KnownClass::Tuple) } @@ -114,7 +114,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// When the base namedtuple's fields were determined dynamically (e.g., from a variable), /// we can't synthesize precise method signatures and should fall back to `NamedTupleFallback`. - pub(crate) fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { + pub fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { self.explicit_bases(db).iter().any(|base| match base { Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) => { !namedtuple.has_known_fields(db) @@ -127,7 +127,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// This covers `@dataclass`-decorated classes, as well as classes created via /// `dataclass_transform` (function-based, metaclass-based, and base-class-based). - pub(crate) fn is_dataclass_like(self, db: &'db dyn Db) -> bool { + pub fn is_dataclass_like(self, db: &'db dyn Db) -> bool { matches!( CodeGeneratorKind::from_class(db, ClassLiteral::Static(self), None), Some(CodeGeneratorKind::DataclassLike(_)) @@ -135,7 +135,7 @@ impl<'db> StaticClassLiteral<'db> { } /// Returns a new [`StaticClassLiteral`] with the given dataclass params, preserving all other fields. - pub(crate) fn with_dataclass_params( + pub fn with_dataclass_params( self, db: &'db dyn Db, dataclass_params: Option>, @@ -157,7 +157,7 @@ impl<'db> StaticClassLiteral<'db> { /// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if /// synthesis is valid. #[salsa::tracked] - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + pub fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { let body_scope = self.body_scope(db); ["__lt__", "__le__", "__gt__", "__ge__"] .iter() @@ -166,7 +166,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns `true` if any class in this class's MRO (excluding `object`) defines an ordering /// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation. - pub(crate) fn has_ordering_method_in_mro( + pub fn has_ordering_method_in_mro( self, db: &'db dyn Db, specialization: Option>, @@ -182,7 +182,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// Note: We use direct scope lookups here to avoid infinite recursion /// through `own_class_member` -> `own_synthesized_member`. - pub(super) fn total_ordering_root_method( + pub fn total_ordering_root_method( self, db: &'db dyn Db, specialization: Option>, @@ -224,7 +224,7 @@ impl<'db> StaticClassLiteral<'db> { None } - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + pub fn generic_context(self, db: &'db dyn Db) -> Option> { // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code // the knowledge that this class is not generic. if self.is_known(db, KnownClass::VersionInfo) { @@ -242,14 +242,14 @@ impl<'db> StaticClassLiteral<'db> { .or_else(|| self.inherited_legacy_generic_context(db)) } - pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + pub fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { self.pep695_generic_context(db).is_some() } #[salsa::tracked(cycle_initial=generic_context_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] - pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option> { + pub fn pep695_generic_context(self, db: &'db dyn Db) -> Option> { let scope = self.body_scope(db); let file = scope.file(db); let parsed = parsed_module(db, file).load(db); @@ -261,7 +261,7 @@ impl<'db> StaticClassLiteral<'db> { }) } - pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { + pub fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { self.explicit_bases(db).iter().find_map(|base| match base { Type::KnownInstance( KnownInstanceType::SubscriptedGeneric(generic_context) @@ -274,10 +274,7 @@ impl<'db> StaticClassLiteral<'db> { #[salsa::tracked(cycle_initial=generic_context_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] - pub(crate) fn inherited_legacy_generic_context( - self, - db: &'db dyn Db, - ) -> Option> { + pub fn inherited_legacy_generic_context(self, db: &'db dyn Db) -> Option> { GenericContext::from_base_classes( db, self.definition(db), @@ -291,7 +288,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns all of the typevars that are referenced in this class's base class list. /// (This is used to ensure that classes do not reference typevars from enclosing /// generic contexts.) - pub(crate) fn typevars_referenced_in_bases( + pub fn typevars_referenced_in_bases( self, db: &'db dyn Db, ) -> FxIndexSet> { @@ -327,11 +324,11 @@ impl<'db> StaticClassLiteral<'db> { } /// Returns the generic context that should be inherited by any constructor methods of this class. - pub(super) fn inherited_generic_context(self, db: &'db dyn Db) -> Option> { + pub fn inherited_generic_context(self, db: &'db dyn Db) -> Option> { self.generic_context(db) } - pub(crate) fn file(self, db: &dyn Db) -> File { + pub fn file(self, db: &dyn Db) -> File { self.body_scope(db).file(db) } @@ -345,13 +342,13 @@ impl<'db> StaticClassLiteral<'db> { scope.node(db).expect_class().node(module) } - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + pub fn definition(self, db: &'db dyn Db) -> Definition<'db> { let body_scope = self.body_scope(db); let index = semantic_index(db, body_scope.file(db)); index.expect_single_definition(body_scope.node(db).expect_class()) } - pub(crate) fn apply_specialization( + pub fn apply_specialization( self, db: &'db dyn Db, f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, @@ -366,7 +363,7 @@ impl<'db> StaticClassLiteral<'db> { } } - pub(crate) fn apply_optional_specialization( + pub fn apply_optional_specialization( self, db: &'db dyn Db, specialization: Option>, @@ -377,7 +374,7 @@ impl<'db> StaticClassLiteral<'db> { }) } - pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { generic_context .default_specialization(db, self.known(db)) @@ -392,7 +389,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns the default specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// applies the default specialization to the class's typevars. - pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { generic_context.default_specialization(db, self.known(db)) }) @@ -401,14 +398,14 @@ impl<'db> StaticClassLiteral<'db> { /// Returns the unknown specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// maps each of the class's typevars to `Unknown`. - pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { generic_context.unknown_specialization(db) }) } /// Returns a specialization of this class where each typevar is mapped to itself. - pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { generic_context.identity_specialization(db) }) @@ -428,7 +425,7 @@ impl<'db> StaticClassLiteral<'db> { /// Were this not a salsa query, then the calling query /// would depend on the class's AST and rerun for every change in that file. #[salsa::tracked(returns(deref), cycle_initial=explicit_bases_cycle_initial, cycle_fn=explicit_bases_cycle_fn, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + pub fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { tracing::trace!( "StaticClassLiteral::explicit_bases_query: {}", self.name(db) @@ -482,7 +479,7 @@ impl<'db> StaticClassLiteral<'db> { } /// Return `Some()` if this class is known to be a [`DisjointBase`], or `None` if it is not. - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + pub fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { if self .known_function_decorators(db) .contains(&KnownFunction::DisjointBase) @@ -513,7 +510,7 @@ impl<'db> StaticClassLiteral<'db> { /// that method for why we do this rather than relying on generalised logic for all /// classes, including the special-cased ones that are included in the [`KnownClass`] /// enum. - pub(crate) fn is_protocol(self, db: &'db dyn Db) -> bool { + pub fn is_protocol(self, db: &'db dyn Db) -> bool { self.known(db) .map(KnownClass::is_protocol) .unwrap_or_else(|| { @@ -560,7 +557,7 @@ impl<'db> StaticClassLiteral<'db> { .collect() } - pub(crate) fn known_function_decorators( + pub fn known_function_decorators( self, db: &'db dyn Db, ) -> impl Iterator + 'db { @@ -572,7 +569,7 @@ impl<'db> StaticClassLiteral<'db> { /// Iterate through the decorators on this class, returning the position of the first one /// that matches the given predicate. - pub(super) fn find_decorator_position( + pub fn find_decorator_position( self, db: &'db dyn Db, predicate: impl Fn(Type<'db>) -> bool, @@ -584,7 +581,7 @@ impl<'db> StaticClassLiteral<'db> { /// Iterate through the decorators on this class, returning the index of the first one /// that is either `@dataclass` or `@dataclass(...)`. - pub(crate) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { + pub fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { self.find_decorator_position(db, |ty| match ty { Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass), Type::DataclassDecorator(_) => true, @@ -593,7 +590,7 @@ impl<'db> StaticClassLiteral<'db> { } /// Is this class final? - pub(crate) fn is_final(self, db: &'db dyn Db) -> bool { + pub fn is_final(self, db: &'db dyn Db) -> bool { self.known_function_decorators(db) .contains(&KnownFunction::Final) || enum_metadata(db, ClassLiteral::Static(self)).is_some() @@ -609,7 +606,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order #[salsa::tracked(returns(as_ref), cycle_initial=static_class_try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn try_mro( + pub fn try_mro( self, db: &'db dyn Db, specialization: Option>, @@ -626,7 +623,7 @@ impl<'db> StaticClassLiteral<'db> { /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(crate) fn iter_mro( + pub fn iter_mro( self, db: &'db dyn Db, specialization: Option>, @@ -635,7 +632,7 @@ impl<'db> StaticClassLiteral<'db> { } /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of( + pub fn is_subclass_of( self, db: &'db dyn Db, specialization: Option>, @@ -662,7 +659,7 @@ impl<'db> StaticClassLiteral<'db> { /// Return `true` if this class is, or inherits from, a `NamedTuple` (inherits from /// `typing.NamedTuple`, either directly or indirectly, including functional forms like /// `NamedTuple("X", ...)`). - pub(crate) fn has_named_tuple_class_in_mro(self, db: &'db dyn Db) -> bool { + pub fn has_named_tuple_class_in_mro(self, db: &'db dyn Db) -> bool { self.iter_mro(db, None) .filter_map(ClassBase::into_class) .any(|base| match base.class_literal(db) { @@ -740,7 +737,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns `Some(true)` for a frozen dataclass-like class, `Some(false)` for a non-frozen one, /// and `None` if the class is not a dataclass-like class, or if the dataclass is neither frozen /// nor non-frozen. - pub(crate) fn is_frozen_dataclass(self, db: &'db dyn Db) -> Option { + pub fn is_frozen_dataclass(self, db: &'db dyn Db) -> Option { // Check if this is a base-class-based transformer that has dataclass_transformer_params directly // attached to it (because it is itself decorated with `@dataclass_transform`), or if this class // has an explicit metaclass that is decorated with `@dataclass_transform`. @@ -803,7 +800,7 @@ impl<'db> StaticClassLiteral<'db> { } /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + pub fn metaclass(self, db: &'db dyn Db) -> Type<'db> { self.try_metaclass(db) .map(|(ty, _)| ty) .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) @@ -813,7 +810,7 @@ impl<'db> StaticClassLiteral<'db> { #[salsa::tracked(cycle_initial=try_metaclass_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] - pub(crate) fn try_metaclass( + pub fn try_metaclass( self, db: &'db dyn Db, ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { @@ -953,7 +950,7 @@ impl<'db> StaticClassLiteral<'db> { /// The member resolves to a member on the class itself or any of its proper superclasses. /// /// TODO: Should this be made private...? - pub(super) fn class_member( + pub fn class_member( self, db: &'db dyn Db, name: &str, @@ -986,7 +983,7 @@ impl<'db> StaticClassLiteral<'db> { member } - pub(super) fn class_member_inner( + pub fn class_member_inner( self, db: &'db dyn Db, specialization: Option>, @@ -996,7 +993,7 @@ impl<'db> StaticClassLiteral<'db> { self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) } - pub(crate) fn class_member_from_mro( + pub fn class_member_from_mro( self, db: &'db dyn Db, name: &str, @@ -1040,7 +1037,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope /// directly. Use [`StaticClassLiteral::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. - pub(super) fn own_class_member( + pub fn own_class_member( self, db: &'db dyn Db, inherited_generic_context: Option>, @@ -1157,7 +1154,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or /// a synthesized `__new__` method for a `NamedTuple`. - pub(crate) fn own_synthesized_member( + pub fn own_synthesized_member( self, db: &'db dyn Db, specialization: Option>, @@ -1915,7 +1912,7 @@ impl<'db> StaticClassLiteral<'db> { /// This is implemented as a separate method because the item definitions on a `TypedDict`-based /// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`, /// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`. - pub(crate) fn typed_dict_member( + pub fn typed_dict_member( self, db: &'db dyn Db, specialization: Option>, @@ -1953,7 +1950,7 @@ impl<'db> StaticClassLiteral<'db> { returns(ref), cycle_initial=|_, _, _, _, _| FxIndexMap::default(), heap_size=get_size2::GetSize::get_heap_size)] - pub(crate) fn fields( + pub fn fields( self, db: &'db dyn Db, specialization: Option>, @@ -1991,7 +1988,7 @@ impl<'db> StaticClassLiteral<'db> { .collect() } - pub(crate) fn validate_members(self, context: &InferContext<'db, '_>) { + pub fn validate_members(self, context: &InferContext<'db, '_>) { let db = context.db(); let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self, None) else { return; @@ -2069,7 +2066,7 @@ impl<'db> StaticClassLiteral<'db> { /// including properties inherited from class-level dataclass parameters (like `kw_only=True`) /// and dataclass-transform parameters (like `kw_only_default=True`). They do not represent /// only what is explicitly specified in each field definition. - pub(crate) fn own_fields( + pub fn own_fields( self, db: &'db dyn Db, specialization: Option>, @@ -2228,7 +2225,7 @@ impl<'db> StaticClassLiteral<'db> { /// Look up an instance attribute (available in `__dict__`) of the given name. /// /// See [`Type::instance_member`] for more details. - pub(super) fn instance_member( + pub fn instance_member( self, db: &'db dyn Db, specialization: Option>, @@ -2278,7 +2275,7 @@ impl<'db> StaticClassLiteral<'db> { cycle_initial=implicit_attribute_initial, heap_size=ruff_memory_usage::heap_size, )] - pub(super) fn implicit_attribute_inner( + pub fn implicit_attribute_inner( db: &'db dyn Db, class_body_scope: ScopeId<'db>, name: String, @@ -2611,7 +2608,7 @@ impl<'db> StaticClassLiteral<'db> { /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { // TODO: There are many things that are not yet implemented here: // - `typing.Final` // - Proper diagnostics @@ -2813,7 +2810,7 @@ impl<'db> StaticClassLiteral<'db> { ) } - pub(super) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { Type::instance(db, ClassType::NonGeneric(self.into())) } @@ -2822,7 +2819,7 @@ impl<'db> StaticClassLiteral<'db> { /// A class definition like this will fail at runtime, /// but we must be resilient to it or we could panic. #[salsa::tracked(cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { + pub fn inheritance_cycle(self, db: &'db dyn Db) -> Option { /// Return `true` if the class is cyclically defined. /// /// Also, populates `visited_classes` with all base classes of `self`. @@ -2875,7 +2872,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns a [`Span`] with the range of the class's header. /// /// See [`Self::header_range`] for more details. - pub(crate) fn header_span(self, db: &'db dyn Db) -> Span { + pub fn header_span(self, db: &'db dyn Db) -> Span { Span::from(self.file(db)).with_range(self.header_range(db)) } @@ -2886,7 +2883,7 @@ impl<'db> StaticClassLiteral<'db> { /// class Foo(Bar, metaclass=Baz): ... /// ^^^^^^^^^^^^^^^^^^^^^^^ /// ``` - pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { + pub fn header_range(self, db: &'db dyn Db) -> TextRange { let class_scope = self.body_scope(db); let module = parsed_module(db, class_scope.file(db)).load(db); let class_node = class_scope.node(db).expect_class().node(&module); @@ -3025,7 +3022,7 @@ impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] -pub(crate) enum InheritanceCycle { +pub enum InheritanceCycle { /// The class is cyclically defined and is a participant in the cycle. /// i.e., it inherits either directly or indirectly from itself. Participant, @@ -3035,7 +3032,7 @@ pub(crate) enum InheritanceCycle { } impl InheritanceCycle { - pub(crate) const fn is_participant(self) -> bool { + pub const fn is_participant(self) -> bool { matches!(self, InheritanceCycle::Participant) } } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index bfc189535ac18..865288c8eb3cc 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -32,11 +32,11 @@ pub enum ClassBase<'db> { } impl<'db> ClassBase<'db> { - pub(crate) const fn unknown() -> Self { + pub const fn unknown() -> Self { Self::Dynamic(DynamicType::Unknown) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -51,7 +51,7 @@ impl<'db> ClassBase<'db> { } } - pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { + pub fn name(self, db: &'db dyn Db) -> &'db str { match self { ClassBase::Class(class) => class.name(db), ClassBase::Dynamic(DynamicType::Any) => "Any", @@ -72,18 +72,18 @@ impl<'db> ClassBase<'db> { } /// Return a `ClassBase` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { + pub fn object(db: &'db dyn Db) -> Self { Self::Class(ClassType::object(db)) } - pub(super) const fn is_typed_dict(self) -> bool { + pub const fn is_typed_dict(self) -> bool { matches!(self, ClassBase::TypedDict) } /// Attempt to resolve `ty` into a `ClassBase`. /// /// Return `None` if `ty` is not an acceptable type for a class base. - pub(super) fn try_from_type( + pub fn try_from_type( db: &'db dyn Db, ty: Type<'db>, subclass: Option>, @@ -272,7 +272,7 @@ impl<'db> ClassBase<'db> { } } - pub(super) fn into_class(self) -> Option> { + pub fn into_class(self) -> Option> { match self { Self::Class(class) => Some(class), Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => None, @@ -280,7 +280,7 @@ impl<'db> ClassBase<'db> { } /// Return the metaclass of this class base. - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + pub fn metaclass(self, db: &'db dyn Db) -> Type<'db> { match self { Self::Class(class) => class.metaclass(db), Self::Dynamic(dynamic) => Type::Dynamic(dynamic), @@ -304,7 +304,7 @@ impl<'db> ClassBase<'db> { } } - pub(crate) fn apply_optional_specialization( + pub fn apply_optional_specialization( self, db: &'db dyn Db, specialization: Option>, @@ -336,7 +336,7 @@ impl<'db> ClassBase<'db> { ) } - pub(super) fn has_cyclic_mro(self, db: &'db dyn Db) -> bool { + pub fn has_cyclic_mro(self, db: &'db dyn Db) -> bool { match self { ClassBase::Class(class) => { let Some((class_literal, specialization)) = class.static_class_literal(db) else { @@ -358,7 +358,7 @@ impl<'db> ClassBase<'db> { } /// Iterate over the MRO of this base - pub(super) fn mro( + pub fn mro( self, db: &'db dyn Db, additional_specialization: Option>, @@ -374,11 +374,11 @@ impl<'db> ClassBase<'db> { } } - pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { + pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { self.display_with(db, DisplaySettings::default()) } - pub(super) fn display_with( + pub fn display_with( self, db: &'db dyn Db, display_settings: DisplaySettings<'db>, diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index f202659367add..a4c9d99e14593 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -90,7 +90,7 @@ use crate::types::{ use crate::{Db, FxIndexMap, FxIndexSet}; /// An extension trait for building constraint sets from [`Option`] values. -pub(crate) trait OptionConstraintsExtension { +pub trait OptionConstraintsExtension { /// Returns a constraint set that is always satisfiable if the option is `None`; otherwise /// applies a function to determine under what constraints the value inside of it holds. fn when_none_or<'db, 'c>( @@ -137,7 +137,7 @@ impl OptionConstraintsExtension for Option { } /// An extension trait for building constraint sets from an [`Iterator`]. -pub(crate) trait IteratorConstraintsExtension { +pub trait IteratorConstraintsExtension { /// Returns the constraints under which any element of the iterator holds. /// /// This method short-circuits; if we encounter any element that @@ -267,7 +267,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { Self::from_node(builder, ALWAYS_TRUE) } - pub(crate) fn from_bool(builder: &'c ConstraintSetBuilder<'db>, b: bool) -> Self { + pub fn from_bool(builder: &'c ConstraintSetBuilder<'db>, b: bool) -> Self { if b { Self::always(builder) } else { @@ -276,7 +276,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } /// Returns a constraint set that constraints a typevar to a particular range of types. - pub(crate) fn constrain_typevar( + pub fn constrain_typevar( db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, @@ -296,12 +296,12 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } /// Returns whether this constraint set never holds - pub(crate) fn is_never_satisfied(self, db: &'db dyn Db) -> bool { + pub fn is_never_satisfied(self, db: &'db dyn Db) -> bool { self.node.is_never_satisfied(db, self.builder) } /// Returns whether this constraint set always holds - pub(crate) fn is_always_satisfied(self, db: &'db dyn Db) -> bool { + pub fn is_always_satisfied(self, db: &'db dyn Db) -> bool { self.node.is_always_satisfied(db, self.builder) } @@ -315,7 +315,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// `(T ≤ list[U]) ∧ (U ≤ list[T])` does not violate our lower/upper bounds restrictions, since /// neither bound _is_ a typevar. And it's not something we can create a specialization from, /// since we would endlessly substitute until we stack overflow. - pub(crate) fn is_cyclic(self, db: &'db dyn Db) -> bool { + pub fn is_cyclic(self, db: &'db dyn Db) -> bool { #[derive(Default)] struct CollectReachability<'db> { reachable_typevars: RefCell>>, @@ -417,7 +417,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns the constraints under which `lhs` is a subtype of `rhs`, assuming that the /// constraints in this constraint set hold. Panics if neither of the types being compared are /// a typevar. (That case is handled by `Type::has_relation_to`.) - pub(crate) fn implies_subtype_of( + pub fn implies_subtype_of( self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -442,7 +442,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// since the constraint set cannot be affected by any typevars that it does not mention. That /// means that those additional typevars trivially satisfy the constraint set, regardless of /// whether they are inferable or not. - pub(crate) fn satisfied_by_all_typevars( + pub fn satisfied_by_all_typevars( &self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -456,7 +456,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn union( + pub fn union( &mut self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -471,7 +471,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn intersect( + pub fn intersect( &mut self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -483,7 +483,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } /// Returns the negation of this constraint set. - pub(crate) fn negate(self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>) -> Self { + pub fn negate(self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>) -> Self { self.verify_builder(builder); Self::from_node(builder, self.node.negate(builder)) } @@ -494,7 +494,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn and( + pub fn and( mut self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -515,7 +515,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn or( + pub fn or( mut self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -534,7 +534,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn implies( + pub fn implies( self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -547,7 +547,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// /// In the result, `self` will appear before `other` according to the `source_order` of the BDD /// nodes. - pub(crate) fn iff( + pub fn iff( self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -562,7 +562,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// abstracted away. Those typevars will be removed from the constraint set, and the constraint /// set will return true whenever there was _any_ specialization of those typevars that /// returned true before. - pub(crate) fn reduce_inferable( + pub fn reduce_inferable( self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -572,7 +572,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { Self::from_node(builder, self.node.exists(db, builder, to_remove)) } - pub(crate) fn solutions( + pub fn solutions( self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -589,18 +589,14 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } #[expect(dead_code)] // Keep this around for debugging purposes - pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { + pub fn display(self, db: &'db dyn Db) -> impl Display { self.node .simplify_for_display(db, self.builder) .display(db, self.builder) } #[expect(dead_code)] // Keep this around for debugging purposes - pub(crate) fn display_graph<'a>( - self, - db: &'db dyn Db, - prefix: &'a dyn Display, - ) -> impl Display + 'a + pub fn display_graph<'a>(self, db: &'db dyn Db, prefix: &'a dyn Display) -> impl Display + 'a where 'db: 'a, 'c: 'a, @@ -638,7 +634,7 @@ impl Debug for ConstraintSet<'_, '_> { /// once we determine that we need _something_ from an inference regions, we always infer _all_ of /// the definitions and expressions in that region, in a stable order. #[derive(Default)] -pub(crate) struct ConstraintSetBuilder<'db> { +pub struct ConstraintSetBuilder<'db> { storage: RefCell>, } @@ -682,14 +678,14 @@ struct ConstraintSetStorage<'db> { } impl<'db> ConstraintSetBuilder<'db> { - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self::default() } /// Creates an [`OwnedConstraintSet`], consuming this builder in the process. You provide a /// callback that constructs a [`ConstraintSet`]. We then package that constraint set up with /// the storage arenas from this builder. - pub(crate) fn into_owned( + pub fn into_owned( self, f: impl for<'c> FnOnce(&'c Self) -> ConstraintSet<'db, 'c>, ) -> OwnedConstraintSet<'db> { @@ -709,7 +705,7 @@ impl<'db> ConstraintSetBuilder<'db> { } /// Loads an [`OwnedConstraintSet`] into this builder. - pub(crate) fn load<'c>( + pub fn load<'c>( &'c self, db: &'db dyn Db, other: &OwnedConstraintSet<'db>, @@ -911,10 +907,10 @@ pub struct ConstraintId; /// An individual constraint in a constraint set. This restricts a single typevar to be within a /// lower and upper bound. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] -pub(crate) struct Constraint<'db> { - pub(crate) typevar: BoundTypeVarInstance<'db>, - pub(crate) lower: Type<'db>, - pub(crate) upper: Type<'db>, +pub struct Constraint<'db> { + pub typevar: BoundTypeVarInstance<'db>, + pub lower: Type<'db>, + pub upper: Type<'db>, } impl ConstraintId { @@ -1248,7 +1244,7 @@ impl ConstraintId { }) } - pub(crate) fn display<'db>( + pub fn display<'db>( self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, @@ -3608,24 +3604,24 @@ impl InteriorNode { } #[derive(Debug)] -pub(crate) enum Solutions<'db, 'c> { +pub enum Solutions<'db, 'c> { Unsatisfiable, Unconstrained, Constrained(Ref<'c, Vec>>), } -pub(crate) type Solution<'db> = Vec>; +pub type Solution<'db> = Vec>; #[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] -pub(crate) struct TypeVarSolution<'db> { - pub(crate) bound_typevar: BoundTypeVarInstance<'db>, - pub(crate) solution: Type<'db>, +pub struct TypeVarSolution<'db> { + pub bound_typevar: BoundTypeVarInstance<'db>, + pub solution: Type<'db>, } /// An assignment of one BDD variable to either `true` or `false`. (When evaluating a BDD, we /// must provide an assignment for each variable present in the BDD.) #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] -pub(crate) enum ConstraintAssignment { +pub enum ConstraintAssignment { Positive(ConstraintId), Negative(ConstraintId), } @@ -4512,7 +4508,7 @@ impl SequentMap { /// The collection of constraints that we know to be true or false at a certain point when /// traversing a BDD. #[derive(Debug)] -pub(crate) struct PathAssignments { +pub struct PathAssignments { map: SequentMap, assignments: FxIndexMap, /// Constraints that we have discovered, mapped to whether we have processed them yet. (This @@ -4612,7 +4608,7 @@ impl PathAssignments { result } - pub(crate) fn positive_constraints(&self) -> impl Iterator + '_ { + pub fn positive_constraints(&self) -> impl Iterator + '_ { self.assignments .iter() .filter_map(|(assignment, source_order)| match assignment { diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index a0f688981e98b..caff6029aa6c8 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -34,7 +34,7 @@ use crate::{ /// It's important that the context is explicitly consumed before dropping by calling /// [`InferContext::finish`] and the returned diagnostics must be stored /// on the current inference result. -pub(crate) struct InferContext<'db, 'ast> { +pub struct InferContext<'db, 'ast> { db: &'db dyn Db, scope: ScopeId<'db>, file: File, @@ -46,7 +46,7 @@ pub(crate) struct InferContext<'db, 'ast> { } impl<'db, 'ast> InferContext<'db, 'ast> { - pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>, module: &'ast ParsedModuleRef) -> Self { + pub fn new(db: &'db dyn Db, scope: ScopeId<'db>, module: &'ast ParsedModuleRef) -> Self { Self { db, scope, @@ -62,16 +62,16 @@ impl<'db, 'ast> InferContext<'db, 'ast> { } /// The file for which the types are inferred. - pub(crate) fn file(&self) -> File { + pub fn file(&self) -> File { self.file } /// The module for which the types are inferred. - pub(crate) fn module(&self) -> &'ast ParsedModuleRef { + pub fn module(&self) -> &'ast ParsedModuleRef { self.module } - pub(crate) fn scope(&self) -> ScopeId<'db> { + pub fn scope(&self) -> ScopeId<'db> { self.scope } @@ -81,7 +81,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// If you're creating a diagnostic with snippets in files /// other than this one, you should create the span directly /// and not use this convenience API. - pub(crate) fn span(&self, ranged: T) -> Span { + pub fn span(&self, ranged: T) -> Span { Span::from(self.file()).with_range(ranged.range()) } @@ -89,21 +89,21 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// the file currently being type checked. /// /// The annotation returned has no message attached to it. - pub(crate) fn secondary(&self, ranged: T) -> Annotation { + pub fn secondary(&self, ranged: T) -> Annotation { Annotation::secondary(self.span(ranged)) } - pub(crate) fn db(&self) -> &'db dyn Db { + pub fn db(&self) -> &'db dyn Db { self.db } - pub(crate) fn extend(&mut self, other: &TypeCheckDiagnostics) { + pub fn extend(&mut self, other: &TypeCheckDiagnostics) { if !self.is_in_multi_inference() { self.diagnostics.get_mut().extend(other); } } - pub(super) fn is_lint_enabled(&self, lint: &'static LintMetadata) -> bool { + pub fn is_lint_enabled(&self, lint: &'static LintMetadata) -> bool { LintDiagnosticGuardBuilder::severity_and_source(self, LintId::of(lint)).is_some() } @@ -135,7 +135,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// /// If callers need to create a non-lint diagnostic, you'll want to use the /// lower level `InferContext::report_diagnostic` routine. - pub(super) fn report_lint<'ctx, T: Ranged>( + pub fn report_lint<'ctx, T: Ranged>( &'ctx self, lint: &'static LintMetadata, ranged: T, @@ -157,7 +157,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// /// Callers should generally prefer adding a lint diagnostic via /// `InferContext::report_lint` whenever possible. - pub(super) fn report_diagnostic<'ctx>( + pub fn report_diagnostic<'ctx>( &'ctx self, id: DiagnosticId, severity: Severity, @@ -168,16 +168,16 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// Returns `true` if the current expression is being inferred for a second /// (or subsequent) time, with a potentially different bidirectional type /// context. - pub(super) fn is_in_multi_inference(&self) -> bool { + pub fn is_in_multi_inference(&self) -> bool { self.multi_inference } /// Set the multi-inference state, returning the previous value. - pub(super) fn set_multi_inference(&mut self, multi_inference: bool) -> bool { + pub fn set_multi_inference(&mut self, multi_inference: bool) -> bool { std::mem::replace(&mut self.multi_inference, multi_inference) } - pub(super) fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) -> InNoTypeCheck { + pub fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) -> InNoTypeCheck { std::mem::replace(&mut self.no_type_check, no_type_check) } @@ -212,12 +212,12 @@ impl<'db, 'ast> InferContext<'db, 'ast> { } /// Are we currently inferring types in a stub file? - pub(crate) fn in_stub(&self) -> bool { + pub fn in_stub(&self) -> bool { self.file.is_stub(self.db()) } #[must_use] - pub(crate) fn finish(mut self) -> TypeCheckDiagnostics { + pub fn finish(mut self) -> TypeCheckDiagnostics { self.bomb.defuse(); let mut diagnostics = self.diagnostics.into_inner(); diagnostics.shrink_to_fit(); @@ -236,7 +236,7 @@ impl fmt::Debug for InferContext<'_, '_> { } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] -pub(crate) enum InNoTypeCheck { +pub enum InNoTypeCheck { /// The inference might be in a `no_type_check` block but only if any /// ancestor function is decorated with `@no_type_check`. #[default] @@ -257,7 +257,7 @@ pub(crate) enum InNoTypeCheck { /// * Some convenience methods for mutating the underlying `Diagnostic` /// in lint context. For example, `LintDiagnosticGuard::set_primary_message` /// will attach a message to the primary span on the diagnostic. -pub(super) struct LintDiagnosticGuard<'db, 'ctx> { +pub struct LintDiagnosticGuard<'db, 'ctx> { /// The typing context. ctx: &'ctx InferContext<'db, 'ctx>, /// The diagnostic that we want to report. @@ -281,7 +281,7 @@ impl LintDiagnosticGuard<'_, '_> { /// /// Callers can add additional primary or secondary annotations via the /// `DerefMut` trait implementation to a `Diagnostic`. - pub(super) fn set_primary_message(&mut self, message: impl IntoDiagnosticMessage) { + pub fn set_primary_message(&mut self, message: impl IntoDiagnosticMessage) { // N.B. It is normally bad juju to define `self` methods // on types that implement `Deref`. Instead, it's idiomatic // to do `fn foo(this: &mut LintDiagnosticGuard)`, which in @@ -314,7 +314,7 @@ impl LintDiagnosticGuard<'_, '_> { /// /// Callers can add additional primary or secondary annotations via the /// `DerefMut` trait implementation to a `Diagnostic`. - pub(super) fn add_primary_tag(&mut self, tag: DiagnosticTag) { + pub fn add_primary_tag(&mut self, tag: DiagnosticTag) { let ann = self.primary_annotation_mut().unwrap(); ann.push_tag(tag); } @@ -402,7 +402,7 @@ impl Drop for LintDiagnosticGuard<'_, '_> { /// When a builder is not returned by `InferContext::report_lint`, then /// it is known that the diagnostic should not be reported. This can happen /// when the diagnostic is disabled or suppressed (among other reasons). -pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> { +pub struct LintDiagnosticGuardBuilder<'db, 'ctx> { ctx: &'ctx InferContext<'db, 'ctx>, id: LintId, severity: Severity, @@ -480,7 +480,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - pub(super) fn into_diagnostic( + pub fn into_diagnostic( self, message: impl std::fmt::Display, ) -> LintDiagnosticGuard<'db, 'ctx> { @@ -509,7 +509,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// this builder further requires a message (with those three things being the /// minimal amount of information with which to construct a diagnostic) before /// one can mutate the diagnostic. -pub(super) struct DiagnosticGuardBuilder<'db, 'ctx> { +pub struct DiagnosticGuardBuilder<'db, 'ctx> { ctx: &'ctx InferContext<'db, 'ctx>, id: DiagnosticId, severity: Severity, @@ -539,7 +539,7 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - pub(super) fn into_diagnostic(self, message: impl std::fmt::Display) -> DiagnosticGuard<'ctx> { + pub fn into_diagnostic(self, message: impl std::fmt::Display) -> DiagnosticGuard<'ctx> { let diag = Diagnostic::new(self.id, self.severity, message); DiagnosticGuard::new(self.ctx.file, &self.ctx.diagnostics, diag) diff --git a/crates/ty_python_semantic/src/types/context_manager.rs b/crates/ty_python_semantic/src/types/context_manager.rs index 5ae29c83b48ef..92ea82b30e761 100644 --- a/crates/ty_python_semantic/src/types/context_manager.rs +++ b/crates/ty_python_semantic/src/types/context_manager.rs @@ -12,7 +12,7 @@ impl<'db> Type<'db> { /// /// This method should only be used outside of type checking because it omits any errors. /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. - pub(super) fn enter(self, db: &'db dyn Db) -> Type<'db> { + pub fn enter(self, db: &'db dyn Db) -> Type<'db> { self.try_enter_with_mode(db, EvaluationMode::Sync) .unwrap_or_else(|err| err.fallback_enter_type(db)) } @@ -21,7 +21,7 @@ impl<'db> Type<'db> { /// /// This method should only be used outside of type checking because it omits any errors. /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. - pub(super) fn aenter(self, db: &'db dyn Db) -> Type<'db> { + pub fn aenter(self, db: &'db dyn Db) -> Type<'db> { self.try_enter_with_mode(db, EvaluationMode::Async) .unwrap_or_else(|err| err.fallback_enter_type(db)) } @@ -34,7 +34,7 @@ impl<'db> Type<'db> { /// with x as y: /// pass /// ``` - pub(super) fn try_enter_with_mode( + pub fn try_enter_with_mode( self, db: &'db dyn Db, mode: EvaluationMode, @@ -92,7 +92,7 @@ impl<'db> Type<'db> { /// Error returned if a type is not (or may not be) a context manager. #[derive(Debug)] -pub(super) enum ContextManagerError<'db> { +pub enum ContextManagerError<'db> { Enter(CallDunderError<'db>, EvaluationMode), Exit { enter_return_type: Type<'db>, @@ -107,7 +107,7 @@ pub(super) enum ContextManagerError<'db> { } impl<'db> ContextManagerError<'db> { - pub(super) fn fallback_enter_type(&self, db: &'db dyn Db) -> Type<'db> { + pub fn fallback_enter_type(&self, db: &'db dyn Db) -> Type<'db> { self.enter_type(db).unwrap_or(Type::unknown()) } @@ -136,7 +136,7 @@ impl<'db> ContextManagerError<'db> { } } - pub(super) fn report_diagnostic( + pub fn report_diagnostic( &self, context: &InferContext<'db, '_>, context_expression_type: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index d279f0f93c207..0986d58f190a6 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -45,7 +45,7 @@ use crate::types::Type; /// ensures we bail out before hitting a stack overflow. const MAX_RECURSION_DEPTH: u32 = 64; -pub(crate) type TypeTransformer<'db, Tag> = CycleDetector, Type<'db>>; +pub type TypeTransformer<'db, Tag> = CycleDetector, Type<'db>>; impl Default for TypeTransformer<'_, Tag> { fn default() -> Self { @@ -57,7 +57,7 @@ impl Default for TypeTransformer<'_, Tag> { } } -pub(crate) type PairVisitor<'db, Tag, C> = CycleDetector, Type<'db>), C>; +pub type PairVisitor<'db, Tag, C> = CycleDetector, Type<'db>), C>; #[derive(Debug)] pub struct CycleDetector { @@ -80,7 +80,7 @@ pub struct CycleDetector { fallback: R, - pub(crate) extra: Extra, + pub extra: Extra, _tag: PhantomData, } @@ -92,7 +92,7 @@ impl CycleDetector CycleDetector { - pub(crate) fn with_extra(fallback: R, extra: Extra) -> Self { + pub fn with_extra(fallback: R, extra: Extra) -> Self { CycleDetector { seen: RefCell::new(FxIndexSet::default()), cache: RefCell::new(FxHashMap::default()), diff --git a/crates/ty_python_semantic/src/types/definition.rs b/crates/ty_python_semantic/src/types/definition.rs index 7ea3b8094cf77..495ecfa327684 100644 --- a/crates/ty_python_semantic/src/types/definition.rs +++ b/crates/ty_python_semantic/src/types/definition.rs @@ -57,7 +57,7 @@ impl TypeDefinition<'_> { } } - pub(super) fn file(&self, db: &dyn Db) -> Option { + pub fn file(&self, db: &dyn Db) -> Option { match self { Self::Module(module) => module.file(db), Self::StaticClass(definition) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 76d27ce353f4c..ac6e30f876c57 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -52,7 +52,7 @@ const RUNTIME_CHECKABLE_DOCS_URL: &str = "https://docs.python.org/3/library/typing.html#typing.runtime_checkable"; /// Registers all known type check lints. -pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { +pub fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&AMBIGUOUS_PROTOCOL_MEMBER); registry.register_lint(&CALL_NON_CALLABLE); registry.register_lint(&CALL_TOP_CALLABLE); @@ -177,7 +177,7 @@ declare_lint! { /// ```python /// 4() # TypeError: 'int' object is not callable /// ``` - pub(crate) static CALL_NON_CALLABLE = { + pub static CALL_NON_CALLABLE = { summary: "detects calls to non-callable objects", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -202,7 +202,7 @@ declare_lint! { /// if callable(x): /// x() # error: We know `x` is callable, but not what arguments it accepts /// ``` - pub(crate) static CALL_TOP_CALLABLE = { + pub static CALL_TOP_CALLABLE = { summary: "detects calls to the top callable type", status: LintStatus::stable("0.0.7"), default_level: Level::Error, @@ -228,7 +228,7 @@ declare_lint! { /// /// A()[0] # TypeError: 'A' object is not subscriptable /// ``` - pub(crate) static POSSIBLY_MISSING_IMPLICIT_CALL = { + pub static POSSIBLY_MISSING_IMPLICIT_CALL = { summary: "detects implicit calls to possibly missing methods", status: LintStatus::stable("0.0.1-alpha.22"), default_level: Level::Warn, @@ -254,7 +254,7 @@ declare_lint! { /// /// f(int) # error /// ``` - pub(crate) static CONFLICTING_ARGUMENT_FORMS = { + pub static CONFLICTING_ARGUMENT_FORMS = { summary: "detects when an argument is used as both a value and a type form in a call", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -279,7 +279,7 @@ declare_lint! { /// /// a = 1 /// ``` - pub(crate) static CONFLICTING_DECLARATIONS = { + pub static CONFLICTING_DECLARATIONS = { summary: "detects conflicting declarations", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -305,7 +305,7 @@ declare_lint! { /// # TypeError: metaclass conflict /// class C(A, B): ... /// ``` - pub(crate) static CONFLICTING_METACLASS = { + pub static CONFLICTING_METACLASS = { summary: "detects conflicting metaclasses", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -331,7 +331,7 @@ declare_lint! { /// ``` /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(crate) static CYCLIC_CLASS_DEFINITION = { + pub static CYCLIC_CLASS_DEFINITION = { summary: "detects cyclic class definitions", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -353,7 +353,7 @@ declare_lint! { /// type A = B /// type B = A /// ``` - pub(crate) static CYCLIC_TYPE_ALIAS_DEFINITION = { + pub static CYCLIC_TYPE_ALIAS_DEFINITION = { summary: "detects cyclic type alias definitions", status: LintStatus::stable("0.0.1-alpha.29"), default_level: Level::Error, @@ -375,7 +375,7 @@ declare_lint! { /// ```python /// 5 / 0 /// ``` - pub(crate) static DIVISION_BY_ZERO = { + pub static DIVISION_BY_ZERO = { summary: "detects division by zero", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Ignore, @@ -396,7 +396,7 @@ declare_lint! { /// /// old_func() # emits [deprecated] diagnostic /// ``` - pub(crate) static DEPRECATED = { + pub static DEPRECATED = { summary: "detects uses of deprecated items", status: LintStatus::stable("0.0.1-alpha.16"), default_level: Level::Warn, @@ -417,7 +417,7 @@ declare_lint! { /// # TypeError: duplicate base class /// class B(A, A): ... /// ``` - pub(crate) static DUPLICATE_BASE = { + pub static DUPLICATE_BASE = { summary: "detects class definitions with duplicate bases", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -449,7 +449,7 @@ declare_lint! { /// _2: KW_ONLY /// d: bytes /// ``` - pub(crate) static DUPLICATE_KW_ONLY = { + pub static DUPLICATE_KW_ONLY = { summary: "detects dataclass definitions with more than one usage of `KW_ONLY`", status: LintStatus::stable("0.0.1-alpha.12"), default_level: Level::Error, @@ -475,7 +475,7 @@ declare_lint! { /// x: int = 1 # Field with default value /// y: str # Error: Required field after field with default /// ``` - pub(crate) static DATACLASS_FIELD_ORDER = { + pub static DATACLASS_FIELD_ORDER = { summary: "detects dataclass definitions with required fields after fields with default values", status: LintStatus::stable("0.0.15"), default_level: Level::Error, @@ -501,7 +501,7 @@ declare_lint! { /// class A: /// def __setattr__(self, name: str, value: object) -> None: ... /// ``` - pub(crate) static INVALID_DATACLASS_OVERRIDE = { + pub static INVALID_DATACLASS_OVERRIDE = { summary: "detects dataclasses with `frozen=True` that have a custom `__setattr__` or `__delattr__` implementation", status: LintStatus::stable("0.0.13"), default_level: Level::Error, @@ -532,7 +532,7 @@ declare_lint! { /// ``` /// /// [explicitly not supported]: https://docs.python.org/3/howto/enum.html#dataclass-support - pub(crate) static INVALID_DATACLASS = { + pub static INVALID_DATACLASS = { summary: "detects invalid `@dataclass` applications", status: LintStatus::stable("0.0.12"), default_level: Level::Error, @@ -614,7 +614,7 @@ declare_lint! { /// - [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order) /// /// [Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(crate) static INSTANCE_LAYOUT_CONFLICT = { + pub static INSTANCE_LAYOUT_CONFLICT = { summary: "detects class definitions that raise `TypeError` due to instance layout conflict", status: LintStatus::stable("0.0.1-alpha.12"), default_level: Level::Error, @@ -642,7 +642,7 @@ declare_lint! { /// class Foo(int, Protocol): ... /// TypeError: Protocols can only inherit from other protocols, got /// ``` - pub(crate) static INVALID_PROTOCOL = { + pub static INVALID_PROTOCOL = { summary: "detects invalid protocol class definitions", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -678,7 +678,7 @@ declare_lint! { /// class SubProto(BaseProto, Protocol): /// a = 42 # fine (declared in superclass) /// ``` - pub(crate) static AMBIGUOUS_PROTOCOL_MEMBER = { + pub static AMBIGUOUS_PROTOCOL_MEMBER = { summary: "detects protocol classes with ambiguous interfaces", status: LintStatus::stable("0.0.1-alpha.20"), default_level: Level::Warn, @@ -726,7 +726,7 @@ declare_lint! { /// ... _asdict = 42 /// AttributeError: Cannot overwrite NamedTuple attribute _asdict /// ``` - pub(crate) static INVALID_NAMED_TUPLE = { + pub static INVALID_NAMED_TUPLE = { summary: "detects invalid `NamedTuple` class definitions", status: LintStatus::stable("0.0.1-alpha.19"), default_level: Level::Error, @@ -750,7 +750,7 @@ declare_lint! { /// ``` /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(crate) static INCONSISTENT_MRO = { + pub static INCONSISTENT_MRO = { summary: "detects class definitions with an inconsistent MRO", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -770,7 +770,7 @@ declare_lint! { /// t = (0, 1, 2) /// t[3] # IndexError: tuple index out of range /// ``` - pub(crate) static INDEX_OUT_OF_BOUNDS = { + pub static INDEX_OUT_OF_BOUNDS = { summary: "detects index out of bounds errors", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -804,7 +804,7 @@ declare_lint! { /// /// carol = Person(name="Carol", age=25) # typo! /// ``` - pub(crate) static INVALID_KEY = { + pub static INVALID_KEY = { summary: "detects invalid subscript accesses or TypedDict literal keys", status: LintStatus::stable("0.0.1-alpha.17"), default_level: Level::Error, @@ -852,7 +852,7 @@ declare_lint! { /// /// ## References /// - [Typing documentation: `@runtime_checkable`](https://docs.python.org/3/library/typing.html#typing.runtime_checkable) - pub(crate) static ISINSTANCE_AGAINST_PROTOCOL = { + pub static ISINSTANCE_AGAINST_PROTOCOL = { summary: "reports invalid runtime checks against protocol classes", status: LintStatus::stable("0.0.14"), default_level: Level::Error, @@ -888,7 +888,7 @@ declare_lint! { /// /// ## References /// - [Typing specification: `TypedDict`](https://typing.python.org/en/latest/spec/typeddict.html) - pub(crate) static ISINSTANCE_AGAINST_TYPED_DICT = { + pub static ISINSTANCE_AGAINST_TYPED_DICT = { summary: "reports runtime checks against `TypedDict` classes", status: LintStatus::stable("0.0.15"), default_level: Level::Error, @@ -909,7 +909,7 @@ declare_lint! { /// def func(x: int): ... /// func("foo") # error: [invalid-argument-type] /// ``` - pub(crate) static INVALID_ARGUMENT_TYPE = { + pub static INVALID_ARGUMENT_TYPE = { summary: "detects call arguments whose type is not assignable to the corresponding typed parameter", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -932,7 +932,7 @@ declare_lint! { /// def func() -> int: /// return "a" # error: [invalid-return-type] /// ``` - pub(crate) static INVALID_RETURN_TYPE = { + pub static INVALID_RETURN_TYPE = { summary: "detects returned values that can't be assigned to the function's annotated return type", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -972,7 +972,7 @@ declare_lint! { /// """A function that does nothing.""" /// pass # error: [empty-body] /// ``` - pub(crate) static EMPTY_BODY = { + pub static EMPTY_BODY = { summary: "detects functions with empty bodies that have a non-`None` return type annotation", status: LintStatus::stable("0.0.14"), default_level: Level::Error, @@ -994,7 +994,7 @@ declare_lint! { /// ``` /// /// [assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable - pub(crate) static INVALID_ASSIGNMENT = { + pub static INVALID_ASSIGNMENT = { summary: "detects invalid assignments", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1024,7 +1024,7 @@ declare_lint! { /// ``` /// /// [Awaitable]: https://docs.python.org/3/library/collections.abc.html#collections.abc.Awaitable - pub(crate) static INVALID_AWAIT = { + pub static INVALID_AWAIT = { summary: "detects awaiting on types that don't support it", status: LintStatus::stable("0.0.1-alpha.19"), default_level: Level::Error, @@ -1042,7 +1042,7 @@ declare_lint! { /// ```python /// class A(42): ... # error: [invalid-base] /// ``` - pub(crate) static INVALID_BASE = { + pub static INVALID_BASE = { summary: "detects class bases that will cause the class definition to raise an exception at runtime", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1075,7 +1075,7 @@ declare_lint! { /// ``` /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(crate) static UNSUPPORTED_BASE = { + pub static UNSUPPORTED_BASE = { summary: "detects class bases that are unsupported as ty could not feasibly calculate the class's MRO", status: LintStatus::stable("0.0.1-alpha.7"), default_level: Level::Warn, @@ -1109,7 +1109,7 @@ declare_lint! { /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order /// [`unsupported-base`]: https://docs.astral.sh/ty/rules/unsupported-base - pub(crate) static UNSUPPORTED_DYNAMIC_BASE = { + pub static UNSUPPORTED_DYNAMIC_BASE = { summary: "detects dynamic class bases that are unsupported as ty could not feasibly calculate the class's MRO", status: LintStatus::stable("0.0.12"), default_level: Level::Ignore, @@ -1130,7 +1130,7 @@ declare_lint! { /// with 1: /// print(2) /// ``` - pub(crate) static INVALID_CONTEXT_MANAGER = { + pub static INVALID_CONTEXT_MANAGER = { summary: "detects expressions used in with statements that don't implement the context manager protocol", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1153,7 +1153,7 @@ declare_lint! { /// ``` /// /// [assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable - pub(crate) static INVALID_DECLARATION = { + pub static INVALID_DECLARATION = { summary: "detects invalid declarations", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1189,7 +1189,7 @@ declare_lint! { /// /// ## Ruff rule /// This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes) - pub(crate) static INVALID_EXCEPTION_CAUGHT = { + pub static INVALID_EXCEPTION_CAUGHT = { summary: "detects exception handlers that catch classes that do not inherit from `BaseException`", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1231,7 +1231,7 @@ declare_lint! { /// - [Typing spec: Enum members](https://typing.python.org/en/latest/spec/enums.html#enum-members) /// /// [typing spec]: https://typing.python.org/en/latest/spec/enums.html#enum-members - pub(crate) static INVALID_ENUM_MEMBER_ANNOTATION = { + pub static INVALID_ENUM_MEMBER_ANNOTATION = { summary: "detects type annotations on enum members", status: LintStatus::stable("0.0.20"), default_level: Level::Warn, @@ -1273,7 +1273,7 @@ declare_lint! { /// /// ## References /// - [Python documentation: Enum](https://docs.python.org/3/library/enum.html) - pub(crate) static INVALID_GENERIC_ENUM = { + pub static INVALID_GENERIC_ENUM = { summary: "detects generic enum classes", status: LintStatus::stable("0.0.12"), default_level: Level::Error, @@ -1304,7 +1304,7 @@ declare_lint! { /// /// ## References /// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) - pub(crate) static INVALID_GENERIC_CLASS = { + pub static INVALID_GENERIC_CLASS = { summary: "detects invalid generic classes", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1332,7 +1332,7 @@ declare_lint! { /// /// ## References /// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) - pub(crate) static INVALID_LEGACY_TYPE_VARIABLE = { + pub static INVALID_LEGACY_TYPE_VARIABLE = { summary: "detects invalid legacy type variables", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1356,7 +1356,7 @@ declare_lint! { /// /// ## References /// - [Typing spec: ParamSpec](https://typing.python.org/en/latest/spec/generics.html#paramspec) - pub(crate) static INVALID_PARAMSPEC = { + pub static INVALID_PARAMSPEC = { summary: "detects invalid ParamSpec usage", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1377,7 +1377,7 @@ declare_lint! { /// IntOrStr = TypeAliasType("IntOrStr", int | str) # okay /// NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal /// ``` - pub(crate) static INVALID_TYPE_ALIAS_TYPE = { + pub static INVALID_TYPE_ALIAS_TYPE = { summary: "detects invalid TypeAliasType definitions", status: LintStatus::stable("0.0.1-alpha.6"), default_level: Level::Error, @@ -1401,7 +1401,7 @@ declare_lint! { /// Bar = NewType(get_name(), int) # error: The first argument to `NewType` must be a string literal /// Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` /// ``` - pub(crate) static INVALID_NEWTYPE = { + pub static INVALID_NEWTYPE = { summary: "detects invalid NewType definitions", status: LintStatus::stable("0.0.1-alpha.27"), default_level: Level::Error, @@ -1428,7 +1428,7 @@ declare_lint! { /// /// ## References /// - [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses) - pub(crate) static INVALID_METACLASS = { + pub static INVALID_METACLASS = { summary: "detects invalid `metaclass=` arguments", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1471,7 +1471,7 @@ declare_lint! { /// /// ## References /// - [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload) - pub(crate) static INVALID_OVERLOAD = { + pub static INVALID_OVERLOAD = { summary: "detects invalid `@overload` usages", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1527,7 +1527,7 @@ declare_lint! { /// /// ## References /// - [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload) - pub(crate) static USELESS_OVERLOAD_BODY = { + pub static USELESS_OVERLOAD_BODY = { summary: "detects `@overload`-decorated functions with non-stub bodies", status: LintStatus::stable("0.0.1-alpha.22"), default_level: Level::Warn, @@ -1547,7 +1547,7 @@ declare_lint! { /// ```python /// def f(a: int = ''): ... /// ``` - pub(crate) static INVALID_PARAMETER_DEFAULT = { + pub static INVALID_PARAMETER_DEFAULT = { summary: "detects default values that can't be assigned to the parameter's annotated type", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1590,7 +1590,7 @@ declare_lint! { /// ## References /// - [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise) /// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) - pub(crate) static INVALID_RAISE = { + pub static INVALID_RAISE = { summary: "detects `raise` statements that raise invalid exceptions or use invalid causes", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1629,7 +1629,7 @@ declare_lint! { /// /// ## References /// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) - pub(crate) static INVALID_SUPER_ARGUMENT = { + pub static INVALID_SUPER_ARGUMENT = { summary: "detects invalid arguments for `super()`", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1653,7 +1653,7 @@ declare_lint! { /// TYPE_CHECKING: str /// TYPE_CHECKING = '' /// ``` - pub(crate) static INVALID_TYPE_CHECKING_CONSTANT = { + pub static INVALID_TYPE_CHECKING_CONSTANT = { summary: "detects invalid `TYPE_CHECKING` constant assignments", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1677,7 +1677,7 @@ declare_lint! { /// b: Annotated[int] # `Annotated` expects at least two arguments /// ``` /// [type expressions]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions - pub(crate) static INVALID_TYPE_FORM = { + pub static INVALID_TYPE_FORM = { summary: "detects invalid type forms", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1699,7 +1699,7 @@ declare_lint! { /// case NotAClass(): # TypeError at runtime: must be a class /// ... /// ``` - pub(crate) static INVALID_MATCH_PATTERN = { + pub static INVALID_MATCH_PATTERN = { summary: "detect invalid match patterns", status: LintStatus::stable("0.0.18"), default_level: Level::Error, @@ -1727,7 +1727,7 @@ declare_lint! { /// class C: /// def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self` /// ``` - pub(crate) static INVALID_TYPE_GUARD_DEFINITION = { + pub static INVALID_TYPE_GUARD_DEFINITION = { summary: "detects malformed type guard functions", status: LintStatus::stable("0.0.1-alpha.11"), default_level: Level::Error, @@ -1755,7 +1755,7 @@ declare_lint! { /// f(*a) # Error /// f(10) # Error /// ``` - pub(crate) static INVALID_TYPE_GUARD_CALL = { + pub static INVALID_TYPE_GUARD_CALL = { summary: "detects type guard function calls that has no narrowing effect", status: LintStatus::stable("0.0.1-alpha.11"), default_level: Level::Error, @@ -1796,7 +1796,7 @@ declare_lint! { /// ``` /// /// [type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar - pub(crate) static INVALID_TYPE_VARIABLE_CONSTRAINTS = { + pub static INVALID_TYPE_VARIABLE_CONSTRAINTS = { summary: "detects invalid type variable constraints", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1821,7 +1821,7 @@ declare_lint! { /// ``` /// /// [type variable]: https://docs.python.org/3/library/typing.html#typing.TypeVar - pub(crate) static INVALID_TYPE_VARIABLE_BOUND = { + pub static INVALID_TYPE_VARIABLE_BOUND = { summary: "detects invalid type variable bounds", status: LintStatus::stable("0.0.15"), default_level: Level::Error, @@ -1847,7 +1847,7 @@ declare_lint! { /// [type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar /// [bound rules]: https://typing.python.org/en/latest/spec/generics.html#bound-rules /// [constraint rules]: https://typing.python.org/en/latest/spec/generics.html#constraint-rules - pub(crate) static INVALID_TYPE_VARIABLE_DEFAULT = { + pub static INVALID_TYPE_VARIABLE_DEFAULT = { summary: "detects invalid type variable defaults", status: LintStatus::stable("0.0.16"), default_level: Level::Error, @@ -1877,7 +1877,7 @@ declare_lint! { /// /// ## References /// - [Typing spec: Scoping rules for type variables](https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables) - pub(crate) static UNBOUND_TYPE_VARIABLE = { + pub static UNBOUND_TYPE_VARIABLE = { summary: "detects type variables used outside of their bound scope", status: LintStatus::stable("0.0.20"), default_level: Level::Error, @@ -1896,7 +1896,7 @@ declare_lint! { /// def func(x: int): ... /// func() # TypeError: func() missing 1 required positional argument: 'x' /// ``` - pub(crate) static MISSING_ARGUMENT = { + pub static MISSING_ARGUMENT = { summary: "detects missing required arguments in a call", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1919,7 +1919,7 @@ declare_lint! { /// def func(x: bool): ... /// func("string") # error: [no-matching-overload] /// ``` - pub(crate) static NO_MATCHING_OVERLOAD = { + pub static NO_MATCHING_OVERLOAD = { summary: "detects calls that do not match any overload", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1937,7 +1937,7 @@ declare_lint! { /// ```python /// 4[1] # TypeError: 'int' object is not subscriptable /// ``` - pub(crate) static NOT_SUBSCRIPTABLE = { + pub static NOT_SUBSCRIPTABLE = { summary: "detects subscripting objects that do not support subscripting", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -1978,7 +1978,7 @@ declare_lint! { /// Foo[int, str] # error: too many arguments /// Bar[int] # error: too few arguments /// ``` - pub(crate) static INVALID_TYPE_ARGUMENTS = { + pub static INVALID_TYPE_ARGUMENTS = { summary: "detects invalid type arguments in generic specialization", status: LintStatus::stable("0.0.1-alpha.29"), default_level: Level::Error, @@ -1998,7 +1998,7 @@ declare_lint! { /// for i in 34: # TypeError: 'int' object is not iterable /// pass /// ``` - pub(crate) static NOT_ITERABLE = { + pub static NOT_ITERABLE = { summary: "detects iteration over an object that is not iterable", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2029,7 +2029,7 @@ declare_lint! { /// not b1 # exception raised here /// b1 < b2 < b1 # exception raised here /// ``` - pub(crate) static UNSUPPORTED_BOOL_CONVERSION = { + pub static UNSUPPORTED_BOOL_CONVERSION = { summary: "detects boolean conversion where the object incorrectly implements `__bool__`", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2050,7 +2050,7 @@ declare_lint! { /// /// f(1, x=2) # Error raised here /// ``` - pub(crate) static PARAMETER_ALREADY_ASSIGNED = { + pub static PARAMETER_ALREADY_ASSIGNED = { summary: "detects multiple arguments for the same parameter", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2072,7 +2072,7 @@ declare_lint! { /// /// A.c # AttributeError: type object 'A' has no attribute 'c' /// ``` - pub(crate) static POSSIBLY_MISSING_ATTRIBUTE = { + pub static POSSIBLY_MISSING_ATTRIBUTE = { summary: "detects references to possibly missing attributes", status: LintStatus::stable("0.0.1-alpha.22"), default_level: Level::Warn, @@ -2102,7 +2102,7 @@ declare_lint! { /// # main.py /// from module import a # ImportError: cannot import name 'a' from 'module' /// ``` - pub(crate) static POSSIBLY_MISSING_IMPORT = { + pub static POSSIBLY_MISSING_IMPORT = { summary: "detects possibly missing imports", status: LintStatus::stable("0.0.1-alpha.22"), default_level: Level::Ignore, @@ -2128,7 +2128,7 @@ declare_lint! { /// /// print(x) # NameError: name 'x' is not defined /// ``` - pub(crate) static POSSIBLY_UNRESOLVED_REFERENCE = { + pub static POSSIBLY_UNRESOLVED_REFERENCE = { summary: "detects references to possibly undefined names", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Ignore, @@ -2151,7 +2151,7 @@ declare_lint! { /// class A: ... /// class B(A): ... # Error raised here /// ``` - pub(crate) static SUBCLASS_OF_FINAL_CLASS = { + pub static SUBCLASS_OF_FINAL_CLASS = { summary: "detects subclasses of final classes", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2178,7 +2178,7 @@ declare_lint! { /// class B(A): /// def foo(self): ... # Error raised here /// ``` - pub(crate) static OVERRIDE_OF_FINAL_METHOD = { + pub static OVERRIDE_OF_FINAL_METHOD = { summary: "detects overrides of final methods", status: LintStatus::stable("0.0.1-alpha.29"), default_level: Level::Error, @@ -2205,7 +2205,7 @@ declare_lint! { /// class B(A): /// X = 2 # Error raised here /// ``` - pub(crate) static OVERRIDE_OF_FINAL_VARIABLE = { + pub static OVERRIDE_OF_FINAL_VARIABLE = { summary: "detects overrides of Final class variables", status: LintStatus::stable("0.0.16"), default_level: Level::Error, @@ -2233,7 +2233,7 @@ declare_lint! { /// @final /// class MyClass: ... /// ``` - pub(crate) static INEFFECTIVE_FINAL = { + pub static INEFFECTIVE_FINAL = { summary: "detects calls to `final()` that type checkers cannot interpret", status: LintStatus::stable("0.0.1-alpha.33"), default_level: Level::Warn, @@ -2259,7 +2259,7 @@ declare_lint! { /// def my_function() -> int: /// return 0 /// ``` - pub(crate) static FINAL_ON_NON_METHOD = { + pub static FINAL_ON_NON_METHOD = { summary: "detects `@final` applied to non-method functions", status: LintStatus::stable("0.0.20"), default_level: Level::Error, @@ -2286,7 +2286,7 @@ declare_lint! { /// # OK: `Final` symbol with a value /// MY_CONSTANT: Final[int] = 1 /// ``` - pub(crate) static FINAL_WITHOUT_VALUE = { + pub static FINAL_WITHOUT_VALUE = { summary: "detects `Final` declarations without a value", status: LintStatus::stable("0.0.15"), default_level: Level::Error, @@ -2321,7 +2321,7 @@ declare_lint! { /// class Derived(Base): # Error: `Derived` does not implement `method` /// pass /// ``` - pub(crate) static ABSTRACT_METHOD_IN_FINAL_CLASS = { + pub static ABSTRACT_METHOD_IN_FINAL_CLASS = { summary: "detects `@final` classes with unimplemented abstract methods", status: LintStatus::stable("0.0.13"), default_level: Level::Error, @@ -2364,7 +2364,7 @@ declare_lint! { /// /// Foo.method() # Error: cannot call abstract classmethod /// ``` - pub(crate) static CALL_ABSTRACT_METHOD = { + pub static CALL_ABSTRACT_METHOD = { summary: "detects calls to abstract methods with trivial bodies on class objects", status: LintStatus::preview("0.0.16"), default_level: Level::Error, @@ -2400,7 +2400,7 @@ declare_lint! { /// @override /// def foo(self): ... # fine: overrides `A.foo` /// ``` - pub(crate) static INVALID_EXPLICIT_OVERRIDE = { + pub static INVALID_EXPLICIT_OVERRIDE = { summary: "detects methods that are decorated with `@override` but do not override any method in a superclass", status: LintStatus::stable("0.0.1-alpha.28"), default_level: Level::Error, @@ -2422,7 +2422,7 @@ declare_lint! { /// assert_type(x, int) # fine /// assert_type(x, str) # error: Actual type does not match asserted type /// ``` - pub(crate) static TYPE_ASSERTION_FAILURE = { + pub static TYPE_ASSERTION_FAILURE = { summary: "detects failed type assertions", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2452,7 +2452,7 @@ declare_lint! { /// # the actual type is `int & ~AlwaysFalsy`, /// # which excludes types like `Literal[0]` /// ``` - pub(crate) static ASSERT_TYPE_UNSPELLABLE_SUBTYPE = { + pub static ASSERT_TYPE_UNSPELLABLE_SUBTYPE = { summary: "detects failed type assertions", status: LintStatus::stable("0.0.14"), default_level: Level::Error, @@ -2473,7 +2473,7 @@ declare_lint! { /// /// f("foo") # Error raised here /// ``` - pub(crate) static TOO_MANY_POSITIONAL_ARGUMENTS = { + pub static TOO_MANY_POSITIONAL_ARGUMENTS = { summary: "detects calls passing too many positional arguments", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2512,7 +2512,7 @@ declare_lint! { /// /// ## References /// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) - pub(crate) static UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS = { + pub static UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS = { summary: "detects invalid `super()` calls where implicit arguments are unavailable.", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2539,7 +2539,7 @@ declare_lint! { /// /// ## References /// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) - pub(crate) static SUPER_CALL_IN_NAMED_TUPLE_METHOD = { + pub static SUPER_CALL_IN_NAMED_TUPLE_METHOD = { summary: "detects `super()` calls in methods of `NamedTuple` classes", status: LintStatus::stable("0.0.1-alpha.30"), default_level: Level::Error, @@ -2578,7 +2578,7 @@ declare_lint! { /// /// f(x=1, y=2) # Error raised here /// ``` - pub(crate) static UNKNOWN_ARGUMENT = { + pub static UNKNOWN_ARGUMENT = { summary: "detects unknown keyword arguments in calls", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2599,7 +2599,7 @@ declare_lint! { /// /// f(x=1) # Error raised here /// ``` - pub(crate) static POSITIONAL_ONLY_PARAMETER_AS_KWARG = { + pub static POSITIONAL_ONLY_PARAMETER_AS_KWARG = { summary: "detects positional-only parameters passed as keyword arguments", status: LintStatus::stable("0.0.1-alpha.22"), default_level: Level::Error, @@ -2621,7 +2621,7 @@ declare_lint! { /// /// A().foo # AttributeError: 'A' object has no attribute 'foo' /// ``` - pub(crate) static UNRESOLVED_ATTRIBUTE = { + pub static UNRESOLVED_ATTRIBUTE = { summary: "detects references to unresolved attributes", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2640,7 +2640,7 @@ declare_lint! { /// ```python /// import foo # ModuleNotFoundError: No module named 'foo' /// ``` - pub(crate) static UNRESOLVED_IMPORT = { + pub static UNRESOLVED_IMPORT = { summary: "detects unresolved imports", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2681,7 +2681,7 @@ declare_lint! { /// /// A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' /// ``` - pub(crate) static UNSUPPORTED_OPERATOR = { + pub static UNSUPPORTED_OPERATOR = { summary: "detects binary, unary, or comparison expressions where the operands don't support the operator", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2708,7 +2708,7 @@ declare_lint! { /// fetch_data() # Warning: coroutine is not awaited /// await fetch_data() # OK /// ``` - pub(crate) static UNUSED_AWAITABLE = { + pub static UNUSED_AWAITABLE = { summary: "detects awaitable objects that are used as expression statements without being awaited", status: LintStatus::preview("0.0.21"), default_level: Level::Warn, @@ -2727,7 +2727,7 @@ declare_lint! { /// l = list(range(10)) /// l[1:10:0] # ValueError: slice step cannot be zero /// ``` - pub(crate) static ZERO_STEPSIZE_IN_SLICE = { + pub static ZERO_STEPSIZE_IN_SLICE = { summary: "detects a slice step size of zero", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2751,7 +2751,7 @@ declare_lint! { /// /// static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness /// ``` - pub(crate) static STATIC_ASSERT_ERROR = { + pub static STATIC_ASSERT_ERROR = { summary: "Failed static assertion", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2779,7 +2779,7 @@ declare_lint! { /// C().instance_var = 3 # okay /// C.instance_var = 3 # error: Cannot assign to instance variable /// ``` - pub(crate) static INVALID_ATTRIBUTE_ACCESS = { + pub static INVALID_ATTRIBUTE_ACCESS = { summary: "Invalid attribute access", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -2800,7 +2800,7 @@ declare_lint! { /// /// cast(int, f()) # Redundant /// ``` - pub(crate) static REDUNDANT_CAST = { + pub static REDUNDANT_CAST = { summary: "detects redundant `cast` calls", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Warn, @@ -2826,7 +2826,7 @@ declare_lint! { /// x: ClassVar[Final[int]] = 1 # redundant /// y: Final[ClassVar[int]] = 1 # redundant /// ``` - pub(crate) static REDUNDANT_FINAL_CLASSVAR = { + pub static REDUNDANT_FINAL_CLASSVAR = { summary: "detects redundant combinations of `ClassVar` and `Final`", status: LintStatus::stable("0.0.18"), default_level: Level::Warn, @@ -2853,7 +2853,7 @@ declare_lint! { /// /// ## References /// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) - pub(crate) static SHADOWED_TYPE_VARIABLE = { + pub static SHADOWED_TYPE_VARIABLE = { summary: "detects type variables that shadow type variables from outer scopes", status: LintStatus::stable("0.0.20"), default_level: Level::Error, @@ -2905,7 +2905,7 @@ declare_lint! { /// def g(): /// print(x) /// ``` - pub(crate) static UNRESOLVED_GLOBAL = { + pub static UNRESOLVED_GLOBAL = { summary: "detects `global` statements with no definition in the global scope", status: LintStatus::stable("0.0.1-alpha.15"), default_level: Level::Warn, @@ -2932,7 +2932,7 @@ declare_lint! { /// /// alice["age"] # KeyError /// ``` - pub(crate) static MISSING_TYPED_DICT_KEY = { + pub static MISSING_TYPED_DICT_KEY = { summary: "detects missing required keys in `TypedDict` constructors", status: LintStatus::stable("0.0.1-alpha.20"), default_level: Level::Error, @@ -2957,7 +2957,7 @@ declare_lint! { /// def bar(self): # error: [invalid-typed-dict-statement] /// pass /// ``` - pub(crate) static INVALID_TYPED_DICT_STATEMENT = { + pub static INVALID_TYPED_DICT_STATEMENT = { summary: "detects invalid statements in `TypedDict` class bodies", status: LintStatus::stable("0.0.9"), default_level: Level::Error, @@ -2986,7 +2986,7 @@ declare_lint! { /// class Bar(TypedDict, **x): # error: [invalid-typed-dict-header] /// ... /// ``` - pub(crate) static INVALID_TYPED_DICT_HEADER = { + pub static INVALID_TYPED_DICT_HEADER = { summary: "detects invalid statements in `TypedDict` class headers", status: LintStatus::stable("0.0.14"), default_level: Level::Error, @@ -3084,7 +3084,7 @@ declare_lint! { /// /// [Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle /// [override]: https://docs.python.org/3/library/typing.html#typing.override - pub(crate) static INVALID_METHOD_OVERRIDE = { + pub static INVALID_METHOD_OVERRIDE = { summary: "detects method definitions that violate the Liskov Substitution Principle", status: LintStatus::stable("0.0.1-alpha.20"), default_level: Level::Error, @@ -3122,7 +3122,7 @@ declare_lint! { /// class NonFrozenChild(FrozenBase): # Error raised here /// y: int /// ``` - pub(crate) static INVALID_FROZEN_DATACLASS_SUBCLASS = { + pub static INVALID_FROZEN_DATACLASS_SUBCLASS = { summary: "detects dataclasses with invalid frozen/non-frozen subclassing", status: LintStatus::stable("0.0.1-alpha.35"), default_level: Level::Error, @@ -3162,7 +3162,7 @@ declare_lint! { /// def __lt__(self, other: "MyClass") -> bool: /// return True /// ``` - pub(crate) static INVALID_TOTAL_ORDERING = { + pub static INVALID_TOTAL_ORDERING = { summary: "detects `@total_ordering` classes without an ordering method", status: LintStatus::stable("0.0.10"), default_level: Level::Error, @@ -3216,7 +3216,7 @@ declare_lint! { /// /// [PEP 484]: https://peps.python.org/pep-0484/#positional-only-arguments /// [PEP 570]: https://peps.python.org/pep-0570/ - pub(crate) static INVALID_LEGACY_POSITIONAL_PARAMETER = { + pub static INVALID_LEGACY_POSITIONAL_PARAMETER = { summary: "detects incorrect usage of the legacy convention for specifying positional-only parameters", status: LintStatus::stable("0.0.15"), default_level: Level::Warn, @@ -3231,41 +3231,41 @@ pub struct TypeCheckDiagnostics { } impl TypeCheckDiagnostics { - pub(crate) fn push(&mut self, diagnostic: Diagnostic) { + pub fn push(&mut self, diagnostic: Diagnostic) { self.diagnostics.push(diagnostic); } - pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) { + pub fn extend(&mut self, other: &TypeCheckDiagnostics) { self.diagnostics.extend_from_slice(&other.diagnostics); self.used_suppressions.extend(&other.used_suppressions); } - pub(super) fn extend_diagnostics(&mut self, diagnostics: impl IntoIterator) { + pub fn extend_diagnostics(&mut self, diagnostics: impl IntoIterator) { self.diagnostics.extend(diagnostics); } - pub(crate) fn mark_used(&mut self, suppression_id: FileSuppressionId) { + pub fn mark_used(&mut self, suppression_id: FileSuppressionId) { self.used_suppressions.insert(suppression_id); } - pub(crate) fn is_used(&self, suppression_id: FileSuppressionId) -> bool { + pub fn is_used(&self, suppression_id: FileSuppressionId) -> bool { self.used_suppressions.contains(&suppression_id) } - pub(crate) fn used_len(&self) -> usize { + pub fn used_len(&self) -> usize { self.used_suppressions.len() } - pub(crate) fn shrink_to_fit(&mut self) { + pub fn shrink_to_fit(&mut self) { self.used_suppressions.shrink_to_fit(); self.diagnostics.shrink_to_fit(); } - pub(crate) fn into_diagnostics(self) -> Vec { + pub fn into_diagnostics(self) -> Vec { self.diagnostics } - pub(crate) fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.diagnostics.is_empty() && self.used_suppressions.is_empty() } @@ -3304,7 +3304,7 @@ impl<'a> IntoIterator for &'a TypeCheckDiagnostics { } /// Emit a diagnostic declaring that an index is out of bounds for a tuple. -pub(super) fn report_index_out_of_bounds( +pub fn report_index_out_of_bounds( context: &InferContext, kind: &'static str, node: AnyNodeRef, @@ -3322,7 +3322,7 @@ pub(super) fn report_index_out_of_bounds( } /// Emit a diagnostic declaring that a type does not support subscripting. -pub(super) fn report_not_subscriptable( +pub fn report_not_subscriptable( context: &InferContext, node: &ast::ExprSubscript, not_subscriptable_ty: Type, @@ -3344,7 +3344,7 @@ pub(super) fn report_not_subscriptable( } } -pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeRef) { +pub fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeRef) { let Some(builder) = context.report_lint(&ZERO_STEPSIZE_IN_SLICE, node) else { return; }; @@ -3353,11 +3353,7 @@ pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeR // We avoid emitting invalid assignment diagnostic for literal assignments to a `TypedDict`, as // they can only occur if we already failed to validate the dict (and emitted some diagnostic). -pub(crate) fn is_invalid_typed_dict_literal( - db: &dyn Db, - target_ty: Type, - source: AnyNodeRef<'_>, -) -> bool { +pub fn is_invalid_typed_dict_literal(db: &dyn Db, target_ty: Type, source: AnyNodeRef<'_>) -> bool { target_ty .filter_union(db, Type::is_typed_dict) .as_typed_dict() @@ -3393,7 +3389,7 @@ fn report_invalid_assignment_with_message<'db, 'ctx: 'db, T: Ranged>( Some(diag) } -pub(super) fn note_numbers_module_not_supported<'db>( +pub fn note_numbers_module_not_supported<'db>( db: &'db dyn Db, diag: &mut Diagnostic, target_ty: Type<'db>, @@ -3422,7 +3418,7 @@ pub(super) fn note_numbers_module_not_supported<'db>( } } -pub(super) fn report_invalid_assignment<'db>( +pub fn report_invalid_assignment<'db>( context: &InferContext<'db, '_>, target_node: AnyNodeRef, definition: Definition<'db>, @@ -3508,7 +3504,7 @@ pub(super) fn report_invalid_assignment<'db>( note_numbers_module_not_supported(context.db(), &mut diag, target_ty, value_ty); } -pub(super) fn report_invalid_attribute_assignment( +pub fn report_invalid_attribute_assignment( context: &InferContext, node: AnyNodeRef, target_ty: Type, @@ -3532,7 +3528,7 @@ pub(super) fn report_invalid_attribute_assignment( ); } -pub(super) fn report_bad_dunder_set_call<'db>( +pub fn report_bad_dunder_set_call<'db>( context: &InferContext<'db, '_>, dunder_set_failure: &CallError<'db>, attribute: &str, @@ -3571,7 +3567,7 @@ pub(super) fn report_bad_dunder_set_call<'db>( } } -pub(super) fn report_invalid_return_type( +pub fn report_invalid_return_type( context: &InferContext, object_range: impl Ranged, return_type_range: impl Ranged, @@ -3600,7 +3596,7 @@ pub(super) fn report_invalid_return_type( ); } -pub(super) fn report_invalid_generator_function_return_type( +pub fn report_invalid_generator_function_return_type( context: &InferContext, return_type_range: TextRange, inferred_return: KnownClass, @@ -3635,7 +3631,7 @@ pub(super) fn report_invalid_generator_function_return_type( diag.info(format_args!("See {link} for more details")); } -pub(super) fn report_implicit_return_type( +pub fn report_implicit_return_type( context: &InferContext, range: impl Ranged, expected_ty: Type, @@ -3706,7 +3702,7 @@ pub(super) fn report_implicit_return_type( } } -pub(super) fn report_invalid_type_checking_constant(context: &InferContext, node: AnyNodeRef) { +pub fn report_invalid_type_checking_constant(context: &InferContext, node: AnyNodeRef) { let Some(builder) = context.report_lint(&INVALID_TYPE_CHECKING_CONSTANT, node) else { return; }; @@ -3715,7 +3711,7 @@ pub(super) fn report_invalid_type_checking_constant(context: &InferContext, node ); } -pub(super) fn report_possibly_unresolved_reference( +pub fn report_possibly_unresolved_reference( context: &InferContext, expr_name_node: &ast::ExprName, ) { @@ -3727,7 +3723,7 @@ pub(super) fn report_possibly_unresolved_reference( builder.into_diagnostic(format_args!("Name `{id}` used when possibly not defined")); } -pub(super) fn report_possibly_missing_attribute( +pub fn report_possibly_missing_attribute( context: &InferContext, target: &ast::ExprAttribute, attribute: &str, @@ -3757,7 +3753,7 @@ pub(super) fn report_possibly_missing_attribute( }; } -pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( +pub fn report_invalid_exception_tuple_caught<'db, 'ast>( context: &InferContext<'db, 'ast>, node: &'ast ast::ExprTuple, node_type: Type<'db>, @@ -3791,7 +3787,7 @@ pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( ); } -pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { +pub fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { return; }; @@ -3822,7 +3818,7 @@ pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast ); } -pub(crate) fn report_invalid_exception_raised( +pub fn report_invalid_exception_raised( context: &InferContext, raised_node: &ast::Expr, raise_type: Type, @@ -3844,7 +3840,7 @@ pub(crate) fn report_invalid_exception_raised( } } -pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { +pub fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { return; }; @@ -3866,7 +3862,7 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast: ); } -pub(crate) fn report_instance_layout_conflict( +pub fn report_instance_layout_conflict( context: &InferContext, header_range: TextRange, base_nodes: Option<&[ast::Expr]>, @@ -3960,7 +3956,7 @@ pub(crate) fn report_instance_layout_conflict( /// Emit a diagnostic for a metaclass conflict where both conflicting metaclasses /// are inherited from base classes. -pub(super) fn report_conflicting_metaclass_from_bases( +pub fn report_conflicting_metaclass_from_bases( context: &InferContext, node: AnyNodeRef, class_name: &str, @@ -3992,15 +3988,10 @@ pub(super) fn report_conflicting_metaclass_from_bases( /// The inner data is an `IndexMap` to ensure that diagnostics regarding conflicting disjoint bases /// are reported in a stable order. #[derive(Debug, Default)] -pub(super) struct IncompatibleBases<'db>(FxIndexMap, IncompatibleBaseInfo<'db>>); +pub struct IncompatibleBases<'db>(FxIndexMap, IncompatibleBaseInfo<'db>>); impl<'db> IncompatibleBases<'db> { - pub(super) fn insert( - &mut self, - base: DisjointBase<'db>, - node_index: usize, - class: ClassLiteral<'db>, - ) { + pub fn insert(&mut self, base: DisjointBase<'db>, node_index: usize, class: ClassLiteral<'db>) { let info = IncompatibleBaseInfo { node_index, originating_base: class, @@ -4015,14 +4006,14 @@ impl<'db> IncompatibleBases<'db> { format_enumeration(bad_base_names) } - pub(super) fn len(&self) -> usize { + pub fn len(&self) -> usize { self.0.len() } /// Two disjoint bases are allowed to coexist in an MRO if one is a subclass of the other. /// This method therefore removes any entry in `self` that is a subclass of one or more /// other entries also contained in `self`. - pub(super) fn remove_redundant_entries(&mut self, db: &'db dyn Db) { + pub fn remove_redundant_entries(&mut self, db: &'db dyn Db) { self.0 = self .0 .iter() @@ -4053,7 +4044,7 @@ impl<'a, 'db> IntoIterator for &'a IncompatibleBases<'db> { /// Information about which class base the "disjoint base" stems from #[derive(Debug, Copy, Clone)] -pub(super) struct IncompatibleBaseInfo<'db> { +pub struct IncompatibleBaseInfo<'db> { /// The index of the problematic base in the [`ast::StmtClassDef`]'s bases list. node_index: usize, @@ -4066,7 +4057,7 @@ pub(super) struct IncompatibleBaseInfo<'db> { originating_base: ClassLiteral<'db>, } -pub(crate) fn report_invalid_arguments_to_annotated( +pub fn report_invalid_arguments_to_annotated( context: &InferContext, subscript: &ast::ExprSubscript, ) { @@ -4079,7 +4070,7 @@ pub(crate) fn report_invalid_arguments_to_annotated( ); } -pub(crate) fn report_invalid_argument_number_to_special_form( +pub fn report_invalid_argument_number_to_special_form( context: &InferContext, subscript: &ast::ExprSubscript, special_form: impl Into, @@ -4100,7 +4091,7 @@ pub(crate) fn report_invalid_argument_number_to_special_form( } } -pub(crate) fn report_bad_argument_to_get_protocol_members( +pub fn report_bad_argument_to_get_protocol_members( context: &InferContext, call: &ast::ExprCall, class: ClassLiteral, @@ -4135,7 +4126,7 @@ pub(crate) fn report_bad_argument_to_get_protocol_members( diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#"); } -pub(crate) fn report_bad_argument_to_protocol_interface( +pub fn report_bad_argument_to_protocol_interface( context: &InferContext, call: &ast::ExprCall, param_type: Type, @@ -4170,7 +4161,7 @@ pub(crate) fn report_bad_argument_to_protocol_interface( diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html"); } -pub(crate) fn report_invalid_arguments_to_callable( +pub fn report_invalid_arguments_to_callable( context: &InferContext, subscript: &ast::ExprSubscript, ) { @@ -4182,7 +4173,7 @@ pub(crate) fn report_invalid_arguments_to_callable( )); } -pub(crate) fn report_invalid_class_match_pattern( +pub fn report_invalid_class_match_pattern( context: &InferContext, pattern_cls: T, cls_ty: Type, @@ -4198,7 +4189,7 @@ pub(crate) fn report_invalid_class_match_pattern( diagnostic.set_primary_message("This will raise `TypeError` at runtime"); } -pub(crate) fn add_type_expression_reference_link<'db, 'ctx>( +pub fn add_type_expression_reference_link<'db, 'ctx>( mut diag: LintDiagnosticGuard<'db, 'ctx>, ) -> LintDiagnosticGuard<'db, 'ctx> { diag.info("See the following page for a reference on valid type expressions:"); @@ -4208,7 +4199,7 @@ pub(crate) fn add_type_expression_reference_link<'db, 'ctx>( diag } -pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( +pub fn report_runtime_check_against_non_runtime_checkable_protocol( context: &InferContext, call: &ast::ExprCall, protocol: ProtocolClass, @@ -4232,7 +4223,7 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( diagnostic.info(format_args!("See {RUNTIME_CHECKABLE_DOCS_URL}")); } -pub(crate) fn report_issubclass_check_against_protocol_with_non_method_members<'db>( +pub fn report_issubclass_check_against_protocol_with_non_method_members<'db>( context: &'db InferContext<'db, '_>, call: &ast::ExprCall, protocol: ProtocolClass<'db>, @@ -4295,7 +4286,7 @@ pub(crate) fn report_issubclass_check_against_protocol_with_non_method_members<' } } -pub(crate) fn report_runtime_check_against_typed_dict( +pub fn report_runtime_check_against_typed_dict( context: &InferContext, call: &ast::ExprCall, class: ClassLiteral, @@ -4312,7 +4303,7 @@ pub(crate) fn report_runtime_check_against_typed_dict( diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); } -pub(crate) fn report_match_pattern_against_non_runtime_checkable_protocol( +pub fn report_match_pattern_against_non_runtime_checkable_protocol( context: &InferContext, pattern_cls: T, protocol: ProtocolClass, @@ -4334,7 +4325,7 @@ pub(crate) fn report_match_pattern_against_non_runtime_checkable_protocol( +pub fn report_match_pattern_against_typed_dict( context: &InferContext, pattern_cls: T, class: ClassLiteral, @@ -4370,7 +4361,7 @@ fn add_non_runtime_checkable_protocol_context<'db>( diagnostic.sub(class_def_diagnostic); } -pub(crate) fn report_attempted_protocol_instantiation( +pub fn report_attempted_protocol_instantiation( context: &InferContext, call: &ast::ExprCall, protocol: ProtocolClass, @@ -4395,7 +4386,7 @@ pub(crate) fn report_attempted_protocol_instantiation( diagnostic.sub(class_def_diagnostic); } -pub(crate) fn report_call_to_abstract_method( +pub fn report_call_to_abstract_method( context: &InferContext, call: &ast::ExprCall, function: FunctionType, @@ -4419,7 +4410,7 @@ pub(crate) fn report_call_to_abstract_method( diag.sub(sub); } -pub(crate) fn report_undeclared_protocol_member( +pub fn report_undeclared_protocol_member( context: &InferContext, definition: Definition, protocol_class: ProtocolClass, @@ -4512,7 +4503,7 @@ pub(crate) fn report_undeclared_protocol_member( )); } -pub(crate) fn report_duplicate_bases( +pub fn report_duplicate_bases( context: &InferContext, class: StaticClassLiteral, duplicate_base_error: &DuplicateBaseError, @@ -4557,7 +4548,7 @@ pub(crate) fn report_duplicate_bases( diagnostic.sub(sub_diagnostic); } -pub(crate) fn report_invalid_or_unsupported_base( +pub fn report_invalid_or_unsupported_base( context: &InferContext, base_node: &ast::Expr, base_type: Type, @@ -4667,7 +4658,7 @@ pub(crate) fn report_invalid_or_unsupported_base( } } -pub(crate) fn report_unsupported_base( +pub fn report_unsupported_base( context: &InferContext, base_node: &ast::Expr, base_type: Type, @@ -4708,7 +4699,7 @@ fn report_invalid_base<'ctx, 'db>( Some(diagnostic) } -pub(crate) fn report_invalid_key_on_typed_dict<'db>( +pub fn report_invalid_key_on_typed_dict<'db>( context: &InferContext<'db, '_>, typed_dict_node: AnyNodeRef, key_node: AnyNodeRef, @@ -4788,7 +4779,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( } } -pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'db>( +pub fn report_namedtuple_field_without_default_after_field_with_default<'db>( context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>, (field, field_def): (&str, Option>), @@ -4837,7 +4828,7 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<' } } -pub(super) fn report_named_tuple_field_with_leading_underscore<'db>( +pub fn report_named_tuple_field_with_leading_underscore<'db>( context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>, field_name: &str, @@ -4871,7 +4862,7 @@ pub(super) fn report_named_tuple_field_with_leading_underscore<'db>( )); } -pub(crate) fn report_missing_typed_dict_key<'db>( +pub fn report_missing_typed_dict_key<'db>( context: &InferContext<'db, '_>, constructor_node: AnyNodeRef, typed_dict_ty: Type<'db>, @@ -4886,7 +4877,7 @@ pub(crate) fn report_missing_typed_dict_key<'db>( } } -pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>( +pub fn report_cannot_pop_required_field_on_typed_dict<'db>( context: &InferContext<'db, '_>, key_node: AnyNodeRef, typed_dict_ty: Type<'db>, @@ -4903,14 +4894,14 @@ pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>( /// Enum representing the reason why a key cannot be deleted from a `TypedDict`. #[derive(Copy, Clone)] -pub(crate) enum TypedDictDeleteErrorKind { +pub enum TypedDictDeleteErrorKind { /// The key exists but is required (not `NotRequired`) RequiredKey, /// The key does not exist in the `TypedDict` UnknownKey, } -pub(crate) fn report_cannot_delete_typed_dict_key<'db>( +pub fn report_cannot_delete_typed_dict_key<'db>( context: &InferContext<'db, '_>, key_node: AnyNodeRef, typed_dict_ty: Type<'db>, @@ -4961,7 +4952,7 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>( } } -pub(crate) fn report_invalid_type_param_order<'db>( +pub fn report_invalid_type_param_order<'db>( context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>, node: &ast::StmtClassDef, @@ -5045,7 +5036,7 @@ pub(crate) fn report_invalid_type_param_order<'db>( } } -pub(crate) fn report_invalid_typevar_default_reference<'db>( +pub fn report_invalid_typevar_default_reference<'db>( context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>, typevar_with_bad_default: TypeVarInstance<'db>, @@ -5092,7 +5083,7 @@ pub(crate) fn report_invalid_typevar_default_reference<'db>( } } -pub(crate) fn report_shadowed_type_variable<'db>( +pub fn report_shadowed_type_variable<'db>( context: &InferContext<'db, '_>, typevar_name: &ast::name::Name, kind: &str, @@ -5129,7 +5120,7 @@ pub(crate) fn report_shadowed_type_variable<'db>( // I tried refactoring this function to placate Clippy, // but it did not improve readability! -- AW. #[expect(clippy::too_many_arguments)] -pub(super) fn report_invalid_method_override<'db>( +pub fn report_invalid_method_override<'db>( context: &InferContext<'db, '_>, member: &str, subclass: ClassType<'db>, @@ -5314,7 +5305,7 @@ pub(super) fn report_invalid_method_override<'db>( } } -pub(super) fn report_overridden_final_method<'db>( +pub fn report_overridden_final_method<'db>( context: &InferContext<'db, '_>, member: &str, subclass_definition: Definition<'db>, @@ -5491,7 +5482,7 @@ pub(super) fn report_overridden_final_method<'db>( } } -pub(super) fn report_overridden_final_variable<'db>( +pub fn report_overridden_final_variable<'db>( context: &InferContext<'db, '_>, member: &str, subclass_definition: Definition<'db>, @@ -5545,7 +5536,7 @@ pub(super) fn report_overridden_final_variable<'db>( } } -pub(super) fn report_unsupported_comparison<'db>( +pub fn report_unsupported_comparison<'db>( context: &InferContext<'db, '_>, error: &UnsupportedComparisonError<'db>, range: TextRange, @@ -5652,7 +5643,7 @@ pub(super) fn report_unsupported_comparison<'db>( } } -pub(super) fn report_unsupported_augmented_assignment<'db>( +pub fn report_unsupported_augmented_assignment<'db>( context: &InferContext<'db, '_>, stmt: &ast::StmtAugAssign, left_ty: Type<'db>, @@ -5672,7 +5663,7 @@ pub(super) fn report_unsupported_augmented_assignment<'db>( ); } -pub(super) fn report_unsupported_binary_operation<'db>( +pub fn report_unsupported_binary_operation<'db>( context: &InferContext<'db, '_>, binary_expression: &ast::ExprBinOp, left_ty: Type<'db>, @@ -5766,7 +5757,7 @@ fn report_unsupported_binary_operation_impl<'a>( Some(diagnostic) } -pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( +pub fn report_bad_frozen_dataclass_inheritance<'db>( context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>, class_node: &ast::StmtClassDef, @@ -5854,7 +5845,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( } } -pub(super) fn report_invalid_total_ordering( +pub fn report_invalid_total_ordering( context: &InferContext<'_, '_>, class: ClassLiteral<'_>, decorator: &ast::Decorator, @@ -5877,7 +5868,7 @@ pub(super) fn report_invalid_total_ordering( /// Reports an invalid `total_ordering(cls)` function call where the class /// does not define any ordering method. -pub(super) fn report_invalid_total_ordering_call( +pub fn report_invalid_total_ordering_call( context: &InferContext<'_, '_>, class: ClassLiteral<'_>, call_expression: &ast::ExprCall, @@ -5908,7 +5899,7 @@ pub(super) fn report_invalid_total_ordering_call( /// misconfigured their Python version. /// /// The function returns `true` if a hint was added, `false` otherwise. -pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( +pub fn hint_if_stdlib_submodule_exists_on_other_versions( db: &dyn Db, diagnostic: &mut Diagnostic, full_submodule_name: &ModuleName, @@ -5958,7 +5949,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( /// standard library and `foo.bar` *does* exist as an attribute on *other* /// Python versions, we add a hint to the diagnostic that the user may have /// misconfigured their Python version. -pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( +pub fn hint_if_stdlib_attribute_exists_on_other_versions( db: &dyn Db, mut diagnostic: LintDiagnosticGuard, value_type: Type, diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index b6b0379a16d20..3b37ae6770af5 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -651,7 +651,7 @@ fn fmt_file_location<'db>( /// /// # Returns /// A vector of path components in order (e.g., `["module", "OuterClass", "InnerClass"]`) -pub(super) fn qualified_name_components_from_scope( +pub fn qualified_name_components_from_scope( db: &dyn Db, file: ruff_db::files::File, file_scope_id: FileScopeId, @@ -1254,7 +1254,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } impl<'db> BoundTypeVarIdentity<'db> { - pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { + pub fn display(self, db: &'db dyn Db) -> impl Display { DisplayBoundTypeVarIdentity { bound_typevar_identity: self, db, @@ -1262,11 +1262,7 @@ impl<'db> BoundTypeVarIdentity<'db> { } } - pub(crate) fn display_with( - self, - db: &'db dyn Db, - settings: DisplaySettings<'db>, - ) -> impl Display { + pub fn display_with(self, db: &'db dyn Db, settings: DisplaySettings<'db>) -> impl Display { DisplayBoundTypeVarIdentity { bound_typevar_identity: self, db, @@ -1299,7 +1295,7 @@ impl Display for DisplayBoundTypeVarIdentity<'_> { } impl<'db> TupleSpec<'db> { - pub(crate) fn display_with<'a>( + pub fn display_with<'a>( &'a self, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1312,7 +1308,7 @@ impl<'db> TupleSpec<'db> { } } -pub(crate) struct DisplayTuple<'a, 'db> { +pub struct DisplayTuple<'a, 'db> { tuple: &'a TupleSpec<'db>, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1394,11 +1390,11 @@ impl Display for DisplayTuple<'_, '_> { impl<'db> OverloadLiteral<'db> { // Not currently used, but useful for debugging. #[expect(dead_code)] - pub(crate) fn display(self, db: &'db dyn Db) -> DisplayOverloadLiteral<'db> { + pub fn display(self, db: &'db dyn Db) -> DisplayOverloadLiteral<'db> { Self::display_with(self, db, DisplaySettings::default()) } - pub(crate) fn display_with( + pub fn display_with( self, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1411,7 +1407,7 @@ impl<'db> OverloadLiteral<'db> { } } -pub(crate) struct DisplayOverloadLiteral<'db> { +pub struct DisplayOverloadLiteral<'db> { literal: OverloadLiteral<'db>, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1445,7 +1441,7 @@ impl Display for DisplayOverloadLiteral<'_> { } impl<'db> FunctionType<'db> { - pub(crate) fn display_with( + pub fn display_with( self, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1458,7 +1454,7 @@ impl<'db> FunctionType<'db> { } } -pub(crate) struct DisplayFunctionType<'db> { +pub struct DisplayFunctionType<'db> { ty: FunctionType<'db>, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1534,11 +1530,11 @@ impl Display for DisplayFunctionType<'_> { } impl<'db> GenericAlias<'db> { - pub(crate) fn display(self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { + pub fn display(self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { self.display_with(db, DisplaySettings::default()) } - pub(crate) fn display_with( + pub fn display_with( self, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1552,7 +1548,7 @@ impl<'db> GenericAlias<'db> { } } -pub(crate) struct DisplayGenericAlias<'db> { +pub struct DisplayGenericAlias<'db> { origin: ClassLiteral<'db>, specialization: Specialization<'db>, db: &'db dyn Db, @@ -1747,7 +1743,7 @@ impl<'db> Specialization<'db> { self.display_short(db, TupleSpecialization::No, DisplaySettings::default()) } - pub(crate) fn display_full(self, db: &'db dyn Db) -> DisplaySpecialization<'db> { + pub fn display_full(self, db: &'db dyn Db) -> DisplaySpecialization<'db> { DisplaySpecialization { specialization: self, db, @@ -1857,11 +1853,11 @@ impl TupleSpecialization { } impl<'db> CallableType<'db> { - pub(crate) fn display<'a>(&'a self, db: &'db dyn Db) -> DisplayCallableType<'a, 'db> { + pub fn display<'a>(&'a self, db: &'db dyn Db) -> DisplayCallableType<'a, 'db> { Self::display_with(self, db, DisplaySettings::default()) } - pub(crate) fn display_with<'a>( + pub fn display_with<'a>( &'a self, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1875,7 +1871,7 @@ impl<'db> CallableType<'db> { } } -pub(crate) struct DisplayCallableType<'a, 'db> { +pub struct DisplayCallableType<'a, 'db> { signatures: &'a CallableSignature<'db>, kind: CallableTypeKind, db: &'db dyn Db, @@ -1934,11 +1930,11 @@ impl Display for DisplayCallableType<'_, '_> { } impl<'db> Signature<'db> { - pub(crate) fn display<'a>(&'a self, db: &'db dyn Db) -> DisplaySignature<'a, 'db> { + pub fn display<'a>(&'a self, db: &'db dyn Db) -> DisplaySignature<'a, 'db> { Self::display_with(self, db, DisplaySettings::default()) } - pub(crate) fn display_with<'a>( + pub fn display_with<'a>( &'a self, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1954,7 +1950,7 @@ impl<'db> Signature<'db> { } } -pub(crate) struct DisplaySignature<'a, 'db> { +pub struct DisplaySignature<'a, 'db> { definition: Option>, generic_context: Option<&'a GenericContext<'db>>, parameters: &'a Parameters<'db>, @@ -1965,7 +1961,7 @@ pub(crate) struct DisplaySignature<'a, 'db> { impl<'db> DisplaySignature<'_, 'db> { /// Get detailed display information including component ranges - pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails { + pub fn to_string_parts(&self) -> SignatureDisplayDetails { let mut f = TypeWriter::Details(TypeDetailsWriter::new()); self.fmt_detailed(&mut f).unwrap(); @@ -1975,7 +1971,7 @@ impl<'db> DisplaySignature<'_, 'db> { } } - pub(crate) fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { + pub fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { !self.return_ty.contains_self(db) && !self .parameters @@ -2069,7 +2065,7 @@ impl Display for DisplaySignature<'_, '_> { /// Details about signature display components, including ranges for parameters and return type #[derive(Debug, Clone)] -pub(crate) struct SignatureDisplayDetails { +pub struct SignatureDisplayDetails { /// The full signature string pub label: String, /// Ranges for each parameter within the label @@ -2707,7 +2703,7 @@ impl Display for DisplayMaybeParenthesizedType<'_> { } } -pub(crate) trait TypeArrayDisplay<'db> { +pub trait TypeArrayDisplay<'db> { fn display_with( &self, db: &'db dyn Db, @@ -2757,7 +2753,7 @@ impl<'db> TypeArrayDisplay<'db> for [Type<'db>] { } } -pub(crate) struct DisplayTypeArray<'b, 'db> { +pub struct DisplayTypeArray<'b, 'db> { types: &'b [Type<'db>], db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -2815,13 +2811,13 @@ impl Display for DisplayStringLiteralType<'_> { } } -pub(crate) struct DisplayKnownInstanceRepr<'db> { - pub(crate) known_instance: KnownInstanceType<'db>, - pub(crate) db: &'db dyn Db, +pub struct DisplayKnownInstanceRepr<'db> { + pub known_instance: KnownInstanceType<'db>, + pub db: &'db dyn Db, } impl<'db> KnownInstanceType<'db> { - pub(crate) fn display_with( + pub fn display_with( self, db: &'db dyn Db, _settings: DisplaySettings<'db>, diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index ab4461f5d46c6..2b40528402dfd 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -16,21 +16,21 @@ use crate::{ }; #[derive(Debug, PartialEq, Eq, salsa::Update)] -pub(crate) struct EnumMetadata<'db> { - pub(crate) members: FxIndexMap>, - pub(crate) aliases: FxHashMap, +pub struct EnumMetadata<'db> { + pub members: FxIndexMap>, + pub aliases: FxHashMap, /// Members whose values were defined using `auto()`. - pub(crate) auto_members: FxHashSet, + pub auto_members: FxHashSet, /// The explicit `_value_` annotation type, if declared. - pub(crate) value_annotation: Option>, + pub value_annotation: Option>, /// The custom `__init__` function, if defined on this enum. /// /// When present, member values are validated by synthesizing a call to /// `__init__` rather than by simple type assignability. - pub(crate) init_function: Option>, + pub init_function: Option>, } impl get_size2::GetSize for EnumMetadata<'_> {} @@ -50,7 +50,7 @@ impl<'db> EnumMetadata<'db> { /// /// Priority: explicit `_value_` annotation, then `__init__` → `Any`, /// then the inferred member value type. - pub(crate) fn value_type(&self, member_name: &Name) -> Option> { + pub fn value_type(&self, member_name: &Name) -> Option> { if !self.members.contains_key(member_name) { return None; } @@ -66,7 +66,7 @@ impl<'db> EnumMetadata<'db> { /// Returns the type of `.name`/`._name_` for a given enum member. /// /// This is always a string literal of the member name. - pub(crate) fn name_type(&self, db: &'db dyn Db, member_name: &Name) -> Option> { + pub fn name_type(&self, db: &'db dyn Db, member_name: &Name) -> Option> { self.members .contains_key(member_name) .then(|| Type::string_literal(db, member_name.as_str())) @@ -78,7 +78,7 @@ impl<'db> EnumMetadata<'db> { /// If there is an explicit `_value_` annotation, returns that. /// If there is a custom `__init__`, returns `Any`. /// Otherwise, returns the union of all member value types. - pub(crate) fn instance_value_type(&self, db: &'db dyn Db) -> Option> { + pub fn instance_value_type(&self, db: &'db dyn Db) -> Option> { if self.members.is_empty() { return None; } @@ -101,7 +101,7 @@ impl<'db> EnumMetadata<'db> { /// narrowed to a specific member (e.g. `x: MyEnum` where `MyEnum` has multiple members). /// /// Returns the union of all member name string literals. - pub(crate) fn instance_name_type(&self, db: &'db dyn Db) -> Option> { + pub fn instance_name_type(&self, db: &'db dyn Db) -> Option> { if self.members.is_empty() { return None; } @@ -114,7 +114,7 @@ impl<'db> EnumMetadata<'db> { Some(union) } - pub(crate) fn resolve_member<'a>(&'a self, name: &'a Name) -> Option<&'a Name> { + pub fn resolve_member<'a>(&'a self, name: &'a Name) -> Option<&'a Name> { if self.members.contains_key(name) { Some(name) } else { @@ -125,7 +125,7 @@ impl<'db> EnumMetadata<'db> { /// Returns the set of names listed in an enum's `_ignore_` attribute. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn enum_ignored_names<'db>(db: &'db dyn Db, scope_id: ScopeId<'db>) -> FxHashSet { +pub fn enum_ignored_names<'db>(db: &'db dyn Db, scope_id: ScopeId<'db>) -> FxHashSet { let use_def_map = use_def_map(db, scope_id); let table = place_table(db, scope_id); @@ -156,10 +156,7 @@ pub(crate) fn enum_ignored_names<'db>(db: &'db dyn Db, scope_id: ScopeId<'db>) - /// List all members of an enum. #[allow(clippy::ref_option, clippy::unnecessary_wraps)] #[salsa::tracked(returns(as_ref), cycle_initial=|_, _, _| Some(EnumMetadata::empty()), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn enum_metadata<'db>( - db: &'db dyn Db, - class: ClassLiteral<'db>, -) -> Option> { +pub fn enum_metadata<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> Option> { let class = match class { ClassLiteral::Static(class) => class, ClassLiteral::Dynamic(..) => { @@ -460,7 +457,7 @@ fn custom_init<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Option( +pub fn enum_member_literals<'a, 'db: 'a>( db: &'db dyn Db, class: ClassLiteral<'db>, exclude_member: Option<&'a Name>, @@ -474,11 +471,11 @@ pub(crate) fn enum_member_literals<'a, 'db: 'a>( }) } -pub(crate) fn is_single_member_enum<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool { +pub fn is_single_member_enum<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool { enum_metadata(db, class).is_some_and(|metadata| metadata.members.len() == 1) } -pub(crate) fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { match ty { Type::ClassLiteral(class_literal) => enum_metadata(db, class_literal).is_some(), _ => false, @@ -490,10 +487,7 @@ pub(crate) fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// /// This is a lighter-weight check than `enum_metadata`, which additionally /// verifies that the class has members. -pub(crate) fn is_enum_class_by_inheritance<'db>( - db: &'db dyn Db, - class: StaticClassLiteral<'db>, -) -> bool { +pub fn is_enum_class_by_inheritance<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> bool { Type::ClassLiteral(ClassLiteral::Static(class)) .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) || class @@ -507,7 +501,7 @@ pub(crate) fn is_enum_class_by_inheritance<'db>( /// returns the inner value, not the `nonmember` wrapper. /// /// Returns `Some(value_type)` if the type is a `nonmember[T]`, otherwise `None`. -pub(crate) fn try_unwrap_nonmember_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +pub fn try_unwrap_nonmember_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { match ty { Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Nonmember) => { Some( diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 4a1dc8ef700b7..021619966af29 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -98,17 +98,17 @@ use crate::{Db, FxOrderSet}; /// /// This can be retrieved via `FunctionType::spans` or /// `Type::function_spans`. -pub(crate) struct FunctionSpans { +pub struct FunctionSpans { /// The span of the entire function "signature." This includes /// the name, parameter list and return type (if present). - pub(crate) signature: Span, + pub signature: Span, /// The span of the function name. i.e., `foo` in `def foo(): ...`. - pub(crate) name: Span, + pub name: Span, /// The span of the parameter list, including the opening and /// closing parentheses. - pub(crate) parameters: Span, + pub parameters: Span, /// The span of the annotated return type, if present. - pub(crate) return_type: Option, + pub return_type: Option, } bitflags! { @@ -136,7 +136,7 @@ bitflags! { impl get_size2::GetSize for FunctionDecorators {} impl FunctionDecorators { - pub(super) fn from_decorator_type(db: &dyn Db, decorator_type: Type) -> Self { + pub fn from_decorator_type(db: &dyn Db, decorator_type: Type) -> Self { match decorator_type { Type::FunctionLiteral(function) => match function.known(db) { Some(KnownFunction::NoTypeCheck) => FunctionDecorators::NO_TYPE_CHECK, @@ -192,12 +192,12 @@ pub struct DataclassTransformerParams<'db> { impl get_size2::GetSize for DataclassTransformerParams<'_> {} /// Whether a function should implicitly be treated as a staticmethod based on its name. -pub(crate) fn is_implicit_staticmethod(function_name: &str) -> bool { +pub fn is_implicit_staticmethod(function_name: &str) -> bool { matches!(function_name, "__new__") } /// Whether a function should implicitly be treated as a classmethod based on its name. -pub(crate) fn is_implicit_classmethod(function_name: &str) -> bool { +pub fn is_implicit_classmethod(function_name: &str) -> bool { matches!(function_name, "__init_subclass__" | "__class_getitem__") } @@ -213,20 +213,20 @@ pub struct OverloadLiteral<'db> { pub name: ast::name::Name, /// Is this a function that we special-case somehow? If so, which one? - pub(crate) known: Option, + pub known: Option, /// The scope that's created by the function, in which the function body is evaluated. - pub(crate) body_scope: ScopeId<'db>, + pub body_scope: ScopeId<'db>, /// A set of special decorators that were applied to this function - pub(crate) decorators: FunctionDecorators, + pub decorators: FunctionDecorators, /// If `Some` then contains the `@warnings.deprecated` - pub(crate) deprecated: Option>, + pub deprecated: Option>, /// The arguments to `dataclass_transformer`, if this function was annotated /// with `@dataclass_transformer(...)`. - pub(crate) dataclass_transformer_params: Option>, + pub dataclass_transformer_params: Option>, } // The Salsa heap is tracked separately. @@ -256,29 +256,29 @@ impl<'db> OverloadLiteral<'db> { self.body_scope(db).file(db) } - pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + pub fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { self.decorators(db).contains(decorator) } - pub(crate) fn is_overload(self, db: &dyn Db) -> bool { + pub fn is_overload(self, db: &dyn Db) -> bool { self.has_known_decorator(db, FunctionDecorators::OVERLOAD) } /// Returns true if this overload is decorated with `@staticmethod`, or if it is implicitly a /// staticmethod. - pub(crate) fn is_staticmethod(self, db: &dyn Db) -> bool { + pub fn is_staticmethod(self, db: &dyn Db) -> bool { self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || is_implicit_staticmethod(self.name(db)) } /// Returns true if this overload is decorated with `@classmethod`, or if it is implicitly a /// classmethod. - pub(crate) fn is_classmethod(self, db: &dyn Db) -> bool { + pub fn is_classmethod(self, db: &dyn Db) -> bool { self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) || is_implicit_classmethod(self.name(db)) } - pub(crate) fn node<'ast>( + pub fn node<'ast>( self, db: &dyn Db, file: File, @@ -296,7 +296,7 @@ impl<'db> OverloadLiteral<'db> { /// Iterate through the decorators on this function, returning the span of the first one /// that matches the given predicate. - pub(super) fn find_decorator_span( + pub fn find_decorator_span( self, db: &'db dyn Db, predicate: impl Fn(Type<'db>) -> bool, @@ -318,11 +318,7 @@ impl<'db> OverloadLiteral<'db> { /// Iterate through the decorators on this function, returning the span of the first one /// that matches the given [`KnownFunction`]. - pub(super) fn find_known_decorator_span( - self, - db: &'db dyn Db, - needle: KnownFunction, - ) -> Option { + pub fn find_known_decorator_span(self, db: &'db dyn Db, needle: KnownFunction) -> Option { self.find_decorator_span(db, |ty| { ty.as_function_literal() .is_some_and(|f| f.is_known(db, needle)) @@ -330,7 +326,7 @@ impl<'db> OverloadLiteral<'db> { } /// Returns the [`FileRange`] of the function's name. - pub(crate) fn focus_range(self, db: &dyn Db, module: &ParsedModuleRef) -> FileRange { + pub fn focus_range(self, db: &dyn Db, module: &ParsedModuleRef) -> FileRange { FileRange::new( self.file(db), self.body_scope(db) @@ -410,7 +406,7 @@ impl<'db> OverloadLiteral<'db> { /// calling query is not in the same file as this function is defined in, then this will create /// a cross-module dependency directly on the full AST which will lead to cache /// over-invalidation. - pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { + pub fn signature(self, db: &'db dyn Db) -> Signature<'db> { let mut signature = self.raw_signature(db); let scope = self.body_scope(db); @@ -436,7 +432,7 @@ impl<'db> OverloadLiteral<'db> { /// calling query is not in the same file as this function is defined in, then this will create /// a cross-module dependency directly on the full AST which will lead to cache /// over-invalidation. - pub(super) fn raw_signature(self, db: &'db dyn Db) -> Signature<'db> { + pub fn raw_signature(self, db: &'db dyn Db) -> Signature<'db> { /// `self` or `cls` can be implicitly positional-only if: /// - It is a method AND /// - No parameters in the method use PEP-570 syntax AND @@ -599,11 +595,7 @@ impl<'db> OverloadLiteral<'db> { raw_signature } - pub(crate) fn parameter_span( - self, - db: &'db dyn Db, - parameter_index: Option, - ) -> (Span, Span) { + pub fn parameter_span(self, db: &'db dyn Db, parameter_index: Option) -> (Span, Span) { let file = self.file(db); let span = Span::from(file); let module = parsed_module(db, file).load(db); @@ -622,7 +614,7 @@ impl<'db> OverloadLiteral<'db> { (name_span, parameter_span) } - pub(crate) fn spans(self, db: &'db dyn Db) -> FunctionSpans { + pub fn spans(self, db: &'db dyn Db) -> FunctionSpans { let file = self.file(db); let span = Span::from(file); let module = parsed_module(db, self.file(db)).load(db); @@ -646,7 +638,7 @@ impl<'db> OverloadLiteral<'db> { /// distinct typevars. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct FunctionLiteral<'db> { - pub(crate) last_definition: OverloadLiteral<'db>, + pub last_definition: OverloadLiteral<'db>, } // The Salsa heap is tracked separately. @@ -794,7 +786,7 @@ impl<'db> FunctionLiteral<'db> { /// statements, or if it is a `Protocol` method that only has a docstring, /// or if it is a `Protocol` method whose body only consists of a single /// `raise NotImplementedError` statement. - pub(super) fn as_abstract_method( + pub fn as_abstract_method( self, db: &'db dyn Db, enclosing_class: ClassType<'db>, @@ -843,7 +835,7 @@ impl<'db> FunctionLiteral<'db> { /// /// Methods defined in stub files are never considered to have trivial bodies, /// since stubs use `...` as a placeholder regardless of the runtime implementation. - pub(crate) fn has_trivial_body(self, db: &'db dyn Db) -> bool { + pub fn has_trivial_body(self, db: &'db dyn Db) -> bool { !self.definition(db).file(db).is_stub(db) && matches!( self.body_kind(db), @@ -854,7 +846,7 @@ impl<'db> FunctionLiteral<'db> { /// Indicates whether a method is explicitly or implicitly abstract. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(super) enum AbstractMethodKind { +pub enum AbstractMethodKind { /// The method is explicitly marked as abstract using `@abstractmethod`. Explicit, /// The method is implicitly abstract due to being in a `Protocol` class without an @@ -866,11 +858,11 @@ pub(super) enum AbstractMethodKind { } impl AbstractMethodKind { - pub(super) const fn is_explicit(self) -> bool { + pub const fn is_explicit(self) -> bool { matches!(self, AbstractMethodKind::Explicit) } - pub(super) const fn is_implicit_due_to_stub_body(self) -> bool { + pub const fn is_implicit_due_to_stub_body(self) -> bool { matches!(self, AbstractMethodKind::ImplicitDueToStubBody) } } @@ -879,7 +871,7 @@ impl AbstractMethodKind { /// generic function. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct FunctionType<'db> { - pub(crate) literal: FunctionLiteral<'db>, + pub literal: FunctionLiteral<'db>, /// Contains a potentially modified signature for this function literal, in case certain operations /// (like type mappings) have been applied to it. @@ -899,7 +891,7 @@ pub struct FunctionType<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for FunctionType<'_> {} -pub(super) fn walk_function_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_function_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, function: FunctionType<'db>, visitor: &V, @@ -916,7 +908,7 @@ pub(super) fn walk_function_type<'db, V: super::visitor::TypeVisitor<'db> + ?Siz #[salsa::tracked] impl<'db> FunctionType<'db> { - pub(crate) fn with_inherited_generic_context( + pub fn with_inherited_generic_context( self, db: &'db dyn Db, inherited_generic_context: GenericContext<'db>, @@ -936,7 +928,7 @@ impl<'db> FunctionType<'db> { ) } - pub(crate) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -957,7 +949,7 @@ impl<'db> FunctionType<'db> { ) } - pub(crate) fn with_dataclass_transformer_params( + pub fn with_dataclass_transformer_params( self, db: &'db dyn Db, params: DataclassTransformerParams<'db>, @@ -973,12 +965,12 @@ impl<'db> FunctionType<'db> { } /// Returns the [`File`] in which this function is defined. - pub(crate) fn file(self, db: &'db dyn Db) -> File { + pub fn file(self, db: &'db dyn Db) -> File { self.literal(db).last_definition(db).file(db) } /// Returns the AST node for this function. - pub(super) fn node<'ast>( + pub fn node<'ast>( self, db: &dyn Db, file: File, @@ -987,15 +979,15 @@ impl<'db> FunctionType<'db> { self.literal(db).last_definition(db).node(db, file, module) } - pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + pub fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { self.literal(db).name(db) } - pub(crate) fn known(self, db: &'db dyn Db) -> Option { + pub fn known(self, db: &'db dyn Db) -> Option { self.literal(db).known(db) } - pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool { + pub fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool { self.known(db) == Some(known_function) } @@ -1004,20 +996,20 @@ impl<'db> FunctionType<'db> { /// Some decorators are expected to appear on every overload; others are expected to appear /// only the implementation or first overload. This method does not check either of those /// conditions. - pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + pub fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { self.literal(db).has_known_decorator(db, decorator) } /// Returns true if this method is decorated with `@classmethod`, or if it is implicitly a /// classmethod. - pub(crate) fn is_classmethod(self, db: &'db dyn Db) -> bool { + pub fn is_classmethod(self, db: &'db dyn Db) -> bool { self.iter_overloads_and_implementation(db) .any(|overload| overload.is_classmethod(db)) } /// Returns true if this method is decorated with `@staticmethod`, or if it is implicitly a /// static method. - pub(crate) fn is_staticmethod(self, db: &'db dyn Db) -> bool { + pub fn is_staticmethod(self, db: &'db dyn Db) -> bool { self.iter_overloads_and_implementation(db) .any(|overload| overload.is_staticmethod(db)) } @@ -1025,10 +1017,7 @@ impl<'db> FunctionType<'db> { /// If the implementation of this function is deprecated, returns the `@warnings.deprecated`. /// /// Checking if an overload is deprecated requires deeper call analysis. - pub(crate) fn implementation_deprecated( - self, - db: &'db dyn Db, - ) -> Option> { + pub fn implementation_deprecated(self, db: &'db dyn Db) -> Option> { self.literal(db).implementation_deprecated(db) } @@ -1040,7 +1029,7 @@ impl<'db> FunctionType<'db> { /// calling query is not in the same file as this function is defined in, then this will create /// a cross-module dependency directly on the full AST which will lead to cache /// over-invalidation. - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + pub fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.literal(db).definition(db) } @@ -1067,11 +1056,7 @@ impl<'db> FunctionType<'db> { /// /// An example of a good use case is to improve /// a diagnostic. - pub(crate) fn parameter_span( - self, - db: &'db dyn Db, - parameter_index: Option, - ) -> (Span, Span) { + pub fn parameter_span(self, db: &'db dyn Db, parameter_index: Option) -> (Span, Span) { self.literal(db).parameter_span(db, parameter_index) } @@ -1088,18 +1073,18 @@ impl<'db> FunctionType<'db> { /// /// An example of a good use case is to improve /// a diagnostic. - pub(crate) fn spans(self, db: &'db dyn Db) -> FunctionSpans { + pub fn spans(self, db: &'db dyn Db) -> FunctionSpans { self.literal(db).spans(db) } /// Returns `true` if this function has a trivial body. - pub(crate) fn has_trivial_body(self, db: &'db dyn Db) -> bool { + pub fn has_trivial_body(self, db: &'db dyn Db) -> bool { self.literal(db).has_trivial_body(db) } /// Returns all of the overload signatures and the implementation definition, if any, of this /// function. The overload signatures will be in source order. - pub(crate) fn overloads_and_implementation( + pub fn overloads_and_implementation( self, db: &'db dyn Db, ) -> (&'db [OverloadLiteral<'db>], Option>) { @@ -1108,14 +1093,14 @@ impl<'db> FunctionType<'db> { /// Returns an iterator of all of the definitions of this function, including both overload /// signatures and any implementation, all in source order. - pub(crate) fn iter_overloads_and_implementation( + pub fn iter_overloads_and_implementation( self, db: &'db dyn Db, ) -> impl DoubleEndedIterator> + 'db { self.literal(db).iter_overloads_and_implementation(db) } - pub(crate) fn first_overload_or_implementation(self, db: &'db dyn Db) -> OverloadLiteral<'db> { + pub fn first_overload_or_implementation(self, db: &'db dyn Db) -> OverloadLiteral<'db> { self.iter_overloads_and_implementation(db) .next() .expect("A function must have at least one overload/implementation") @@ -1134,7 +1119,7 @@ impl<'db> FunctionType<'db> { /// Were this not a salsa query, then the calling query /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked(returns(ref), cycle_initial=|_, _, _| CallableSignature::single(Signature::bottom()), heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> { + pub fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> { self.updated_signature(db) .cloned() .unwrap_or_else(|| self.literal(db).signature(db)) @@ -1153,7 +1138,7 @@ impl<'db> FunctionType<'db> { returns(ref), cycle_initial=last_definition_signature_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] - pub(crate) fn last_definition_signature(self, db: &'db dyn Db) -> Signature<'db> { + pub fn last_definition_signature(self, db: &'db dyn Db) -> Signature<'db> { self.updated_last_definition_signature(db) .cloned() .unwrap_or_else(|| self.literal(db).last_definition_signature(db)) @@ -1164,12 +1149,12 @@ impl<'db> FunctionType<'db> { returns(ref), cycle_initial=last_definition_signature_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] - pub(crate) fn last_definition_raw_signature(self, db: &'db dyn Db) -> Signature<'db> { + pub fn last_definition_raw_signature(self, db: &'db dyn Db) -> Signature<'db> { self.literal(db).last_definition_raw_signature(db) } /// Convert the `FunctionType` into a [`CallableType`]. - pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { + pub fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { let kind = if self.is_classmethod(db) { CallableTypeKind::ClassMethodLike } else if self.is_staticmethod(db) { @@ -1181,7 +1166,7 @@ impl<'db> FunctionType<'db> { } /// Convert the `FunctionType` into a [`BoundMethodType`]. - pub(crate) fn into_bound_method_type( + pub fn into_bound_method_type( self, db: &'db dyn Db, self_instance: Type<'db>, @@ -1190,7 +1175,7 @@ impl<'db> FunctionType<'db> { } #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -1217,7 +1202,7 @@ impl<'db> FunctionType<'db> { ) } - pub(crate) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -1230,7 +1215,7 @@ impl<'db> FunctionType<'db> { } } - pub(crate) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -1253,7 +1238,7 @@ impl<'db> FunctionType<'db> { )) } - pub(super) fn as_abstract_method( + pub fn as_abstract_method( self, db: &'db dyn Db, enclosing_class: ClassType<'db>, @@ -1559,7 +1544,7 @@ fn last_definition_signature_cycle_initial<'db>( /// /// In all cases, we allow a docstring as the first statement in the function body; /// the analysis is only done on the remaining statements if the first is a docstring. -pub(super) fn function_body_kind<'db>( +pub fn function_body_kind<'db>( db: &'db dyn Db, node: &ast::StmtFunctionDef, infer_type: impl Fn(&ast::Expr) -> Type<'db>, @@ -1605,7 +1590,7 @@ pub(super) fn function_body_kind<'db>( /// Classification of function body kinds. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) enum FunctionBodyKind { +pub enum FunctionBodyKind { /// The function body only consists of `...`, `pass`, and/or a docstring. Stub, /// The function body consists of a single `raise NotImplementedError` statement, @@ -1745,7 +1730,7 @@ impl KnownFunction { } } - pub(crate) fn try_from_definition_and_name<'db>( + pub fn try_from_definition_and_name<'db>( db: &'db dyn Db, definition: Definition<'db>, name: &str, @@ -1826,7 +1811,7 @@ impl KnownFunction { /// Evaluate a call to this known function, and emit any diagnostics that are necessary /// as a result of the call. - pub(super) fn check_call<'db>( + pub fn check_call<'db>( self, context: &InferContext<'db, '_>, overload: &mut Binding<'db>, @@ -2238,13 +2223,13 @@ impl KnownFunction { } } - pub(crate) fn name(self) -> &'static str { + pub fn name(self) -> &'static str { self.into() } } #[cfg(test)] -pub(crate) mod tests { +pub mod tests { use strum::IntoEnumIterator; use super::*; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index f4c08d30943d0..f6f583074600c 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -37,7 +37,7 @@ use crate::{Db, FxIndexMap, FxOrderMap, FxOrderSet}; /// Returns an iterator of any generic context introduced by the given scope or any enclosing /// scope. -pub(crate) fn enclosing_generic_contexts<'db>( +pub fn enclosing_generic_contexts<'db>( db: &'db dyn Db, index: &SemanticIndex<'db>, scope: FileScopeId, @@ -61,7 +61,7 @@ pub(crate) fn enclosing_generic_contexts<'db>( /// If no enclosing scope has already bound the typevar, we might be in a syntactic position that /// is about to bind it (indicated by a non-`None` `typevar_binding_context`), in which case we /// bind the typevar with that new binding context. -pub(crate) fn bind_typevar<'db>( +pub fn bind_typevar<'db>( db: &'db dyn Db, index: &SemanticIndex<'db>, containing_scope: FileScopeId, @@ -120,7 +120,7 @@ pub(crate) fn bind_typevar<'db>( } /// Create a `typing.Self` type variable for a given class. -pub(crate) fn typing_self<'db>( +pub fn typing_self<'db>( db: &'db dyn Db, scope_id: ScopeId, typevar_binding_context: Option>, @@ -207,7 +207,7 @@ pub(crate) fn typing_self<'db>( } #[derive(Clone, Copy, Debug)] -pub(crate) enum InferableTypeVars<'a, 'db> { +pub enum InferableTypeVars<'a, 'db> { None, One(&'a FxHashSet>), Two( @@ -217,11 +217,7 @@ pub(crate) enum InferableTypeVars<'a, 'db> { } impl<'db> BoundTypeVarInstance<'db> { - pub(crate) fn is_inferable( - self, - db: &'db dyn Db, - inferable: InferableTypeVars<'_, 'db>, - ) -> bool { + pub fn is_inferable(self, db: &'db dyn Db, inferable: InferableTypeVars<'_, 'db>) -> bool { match inferable { InferableTypeVars::None => false, InferableTypeVars::One(typevars) => typevars.contains(&self.identity(db)), @@ -233,7 +229,7 @@ impl<'db> BoundTypeVarInstance<'db> { } impl<'a, 'db> InferableTypeVars<'a, 'db> { - pub(crate) fn merge(&'a self, other: &'a InferableTypeVars<'a, 'db>) -> Self { + pub fn merge(&'a self, other: &'a InferableTypeVars<'a, 'db>) -> Self { match (self, other) { (InferableTypeVars::None, other) | (other, InferableTypeVars::None) => *other, _ => InferableTypeVars::Two(self, other), @@ -242,7 +238,7 @@ impl<'a, 'db> InferableTypeVars<'a, 'db> { // This is not an IntoIterator implementation because I have no desire to try to name the // iterator type. - pub(crate) fn iter(self) -> impl Iterator> { + pub fn iter(self) -> impl Iterator> { match self { InferableTypeVars::None => Either::Left(Either::Left(std::iter::empty())), InferableTypeVars::One(typevars) => Either::Right(typevars.iter().copied()), @@ -256,7 +252,7 @@ impl<'a, 'db> InferableTypeVars<'a, 'db> { // Keep this around for debugging purposes #[expect(dead_code)] - pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display { + pub fn display(&self, db: &'db dyn Db) -> impl Display { fn find_typevars<'db>( result: &mut FxHashSet>, inferable: &InferableTypeVars<'_, 'db>, @@ -290,7 +286,7 @@ pub struct GenericContext<'db> { variables_inner: FxOrderMap, BoundTypeVarInstance<'db>>, } -pub(super) fn walk_generic_context<'db, V: TypeVisitor<'db> + ?Sized>( +pub fn walk_generic_context<'db, V: TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, context: GenericContext<'db>, visitor: &V, @@ -305,7 +301,7 @@ impl get_size2::GetSize for GenericContext<'_> {} impl<'db> GenericContext<'db> { /// Creates a generic context from a list of PEP-695 type parameters. - pub(crate) fn from_type_params( + pub fn from_type_params( db: &'db dyn Db, index: &'db SemanticIndex<'db>, binding_context: Definition<'db>, @@ -319,7 +315,7 @@ impl<'db> GenericContext<'db> { } /// Creates a generic context from a list of `BoundTypeVarInstance`s. - pub(crate) fn from_typevar_instances( + pub fn from_typevar_instances( db: &'db dyn Db, type_params: impl IntoIterator>, ) -> Self { @@ -334,7 +330,7 @@ impl<'db> GenericContext<'db> { /// Merge this generic context with another, returning a new generic context that /// contains type variables from both contexts. - pub(crate) fn merge(self, db: &'db dyn Db, other: Self) -> Self { + pub fn merge(self, db: &'db dyn Db, other: Self) -> Self { Self::from_typevar_instances( db, self.variables_inner(db) @@ -344,7 +340,7 @@ impl<'db> GenericContext<'db> { ) } - pub(crate) fn merge_optional( + pub fn merge_optional( db: &'db dyn Db, left: Option, right: Option, @@ -356,7 +352,7 @@ impl<'db> GenericContext<'db> { } } - pub(crate) fn remove_self( + pub fn remove_self( self, db: &'db dyn Db, binding_context: Option>, @@ -384,7 +380,7 @@ impl<'db> GenericContext<'db> { /// In this example, `method`'s generic context binds `Self` and `T`, but its inferable set /// also includes `A@C`. This is needed because at each call site, we need to infer the /// specialized class instance type whose method is being invoked. - pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> { + pub fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> { #[derive(Default)] struct CollectTypeVars<'db> { typevars: RefCell>>, @@ -437,7 +433,7 @@ impl<'db> GenericContext<'db> { InferableTypeVars::One(inferable_typevars_inner(db, self)) } - pub(crate) fn variables( + pub fn variables( self, db: &'db dyn Db, ) -> impl ExactSizeIterator> + Clone { @@ -453,7 +449,7 @@ impl<'db> GenericContext<'db> { /// class Bar[T, **P]: ... # false /// class Baz[T]: ... # false /// ``` - pub(crate) fn exactly_one_paramspec(self, db: &'db dyn Db) -> bool { + pub fn exactly_one_paramspec(self, db: &'db dyn Db) -> bool { self.variables(db) .exactly_one() .is_ok_and(|bound_typevar| bound_typevar.is_paramspec(db)) @@ -491,7 +487,7 @@ impl<'db> GenericContext<'db> { /// Creates a generic context from the legacy `TypeVar`s that appear in a function parameter /// list. - pub(crate) fn from_function_params( + pub fn from_function_params( db: &'db dyn Db, definition: Definition<'db>, parameters: &Parameters<'db>, @@ -515,7 +511,7 @@ impl<'db> GenericContext<'db> { Some(Self::from_typevar_instances(db, variables)) } - pub(crate) fn merge_pep695_and_legacy( + pub fn merge_pep695_and_legacy( db: &'db dyn Db, pep695_generic_context: Option, legacy_generic_context: Option, @@ -539,7 +535,7 @@ impl<'db> GenericContext<'db> { /// Creates a generic context from the legacy `TypeVar`s that appear in class's base class /// list. - pub(crate) fn from_base_classes( + pub fn from_base_classes( db: &'db dyn Db, definition: Definition<'db>, bases: impl Iterator>, @@ -554,7 +550,7 @@ impl<'db> GenericContext<'db> { Some(Self::from_typevar_instances(db, variables)) } - pub(crate) fn remove_callable_only_typevars( + pub fn remove_callable_only_typevars( db: &'db dyn Db, generic_context: Option, parameters: &Parameters<'db>, @@ -751,11 +747,11 @@ impl<'db> GenericContext<'db> { (generic_context, return_type) } - pub(crate) fn len(self, db: &'db dyn Db) -> usize { + pub fn len(self, db: &'db dyn Db) -> usize { self.variables_inner(db).len() } - pub(crate) fn default_specialization( + pub fn default_specialization( self, db: &'db dyn Db, known_class: Option, @@ -775,12 +771,12 @@ impl<'db> GenericContext<'db> { } /// Returns a specialization of this generic context where each typevar is mapped to itself. - pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> { + pub fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> { let types: Vec = self.variables(db).map(Type::TypeVar).collect(); self.specialize(db, types) } - pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> { + pub fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> { match self.len(db) { 0 => self.specialize(db, &[]), 1 => self.specialize(db, &[Type::unknown(); 1]), @@ -789,13 +785,13 @@ impl<'db> GenericContext<'db> { } } - pub(crate) fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool { + pub fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool { let other_variables = other.variables_inner(db); self.variables(db) .all(|bound_typevar| other_variables.contains_key(&bound_typevar.identity(db))) } - pub(crate) fn binds_named_typevar( + pub fn binds_named_typevar( self, db: &'db dyn Db, name: &'db ast::name::Name, @@ -804,7 +800,7 @@ impl<'db> GenericContext<'db> { .find(|self_bound_typevar| self_bound_typevar.typevar(db).name(db) == name) } - pub(crate) fn binds_typevar( + pub fn binds_typevar( self, db: &'db dyn Db, typevar: TypeVarInstance<'db>, @@ -825,7 +821,7 @@ impl<'db> GenericContext<'db> { /// otherwise, you will be left with a partial specialization. (Use /// [`specialize_recursive`](Self::specialize_recursive) if your types might mention typevars /// in this generic context.) - pub(crate) fn specialize<'t, T>(self, db: &'db dyn Db, types: T) -> Specialization<'db> + pub fn specialize<'t, T>(self, db: &'db dyn Db, types: T) -> Specialization<'db> where T: Into]>>, 'db: 't, @@ -841,7 +837,7 @@ impl<'db> GenericContext<'db> { /// /// If any provided type is `None`, we will use the corresponding typevar's default type. You /// are allowed to provide types that mention the typevars in this generic context. - pub(crate) fn specialize_recursive(self, db: &'db dyn Db, types: I) -> Specialization<'db> + pub fn specialize_recursive(self, db: &'db dyn Db, types: I) -> Specialization<'db> where I: IntoIterator>>, I::IntoIter: ExactSizeIterator, @@ -889,7 +885,7 @@ impl<'db> GenericContext<'db> { } /// Creates a specialization of this generic context for the `tuple` class. - pub(crate) fn specialize_tuple( + pub fn specialize_tuple( self, db: &'db dyn Db, element_type: Type<'db>, @@ -956,7 +952,7 @@ impl<'db> GenericContext<'db> { /// Creates a specialization of this generic context. Panics if the length of `types` does not /// match the number of typevars in the generic context. If any provided type is `None`, we /// will use the corresponding typevar's default type. - pub(crate) fn specialize_partial(self, db: &'db dyn Db, types: I) -> Specialization<'db> + pub fn specialize_partial(self, db: &'db dyn Db, types: I) -> Specialization<'db> where I: IntoIterator>>, I::IntoIter: ExactSizeIterator, @@ -971,9 +967,9 @@ impl<'db> GenericContext<'db> { /// the lexically containing context. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct Specialization<'db> { - pub(crate) generic_context: GenericContext<'db>, + pub generic_context: GenericContext<'db>, #[returns(deref)] - pub(crate) types: Box<[Type<'db>]>, + pub types: Box<[Type<'db>]>, /// The materialization kind of the specialization. For example, given an invariant /// generic type `A`, `Top[A[Any]]` is a supertype of all materializations of `A[Any]`, /// and is represented here with `Some(MaterializationKind::Top)`. Similarly, @@ -981,7 +977,7 @@ pub struct Specialization<'db> { /// with `Some(MaterializationKind::Bottom)`. /// The `materialization_kind` field may be non-`None` only if the specialization contains /// dynamic types in invariant positions. - pub(crate) materialization_kind: Option, + pub materialization_kind: Option, /// For specializations of `tuple`, we also store more detailed information about the tuple's /// elements, above what the class's (single) typevar can represent. @@ -991,7 +987,7 @@ pub struct Specialization<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for Specialization<'_> {} -pub(super) fn walk_specialization<'db, V: TypeVisitor<'db> + ?Sized>( +pub fn walk_specialization<'db, V: TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, specialization: Specialization<'db>, visitor: &V, @@ -1217,11 +1213,7 @@ fn has_relation_in_invariant_position<'db, 'c>( impl<'db> Specialization<'db> { /// Restricts this specialization to only include the typevars in a generic context. If the /// specialization does not include all of those typevars, returns `None`. - pub(crate) fn restrict( - self, - db: &'db dyn Db, - generic_context: GenericContext<'db>, - ) -> Option { + pub fn restrict(self, db: &'db dyn Db, generic_context: GenericContext<'db>) -> Option { let self_variables = self.generic_context(db).variables_inner(db); let self_types = self.types(db); let restricted_variables = generic_context.variables(db); @@ -1241,13 +1233,13 @@ impl<'db> Specialization<'db> { } /// Returns the tuple spec for a specialization of the `tuple` class. - pub(crate) fn tuple(self, db: &'db dyn Db) -> Option<&'db TupleSpec<'db>> { + pub fn tuple(self, db: &'db dyn Db) -> Option<&'db TupleSpec<'db>> { self.tuple_inner(db).map(|tuple_type| tuple_type.tuple(db)) } /// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this /// mapping. - pub(crate) fn get( + pub fn get( self, db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>, @@ -1272,7 +1264,7 @@ impl<'db> Specialization<'db> { /// `{U: int}`, we can apply the second specialization to the first, resulting in `T: int`. /// That lets us produce the generic alias `A[int]`, which is the corresponding entry in the /// MRO of `B[int]`. - pub(crate) fn apply_specialization(self, db: &'db dyn Db, other: Specialization<'db>) -> Self { + pub fn apply_specialization(self, db: &'db dyn Db, other: Specialization<'db>) -> Self { let new_specialization = self.apply_type_mapping( db, &TypeMapping::ApplySpecialization(ApplySpecialization::Specialization(other)), @@ -1287,7 +1279,7 @@ impl<'db> Specialization<'db> { } } - pub(crate) fn apply_type_mapping<'a>( + pub fn apply_type_mapping<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -1295,7 +1287,7 @@ impl<'db> Specialization<'db> { self.apply_type_mapping_impl(db, type_mapping, &[], &ApplyTypeMappingVisitor::default()) } - pub(crate) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -1354,7 +1346,7 @@ impl<'db> Specialization<'db> { } /// Applies an optional specialization to this specialization. - pub(crate) fn apply_optional_specialization( + pub fn apply_optional_specialization( self, db: &'db dyn Db, other: Option>, @@ -1371,7 +1363,7 @@ impl<'db> Specialization<'db> { /// typevar to a known type, those types are unioned together. /// /// Panics if the two specializations are not for the same generic context. - pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self { + pub fn combine(self, db: &'db dyn Db, other: Self) -> Self { let generic_context = self.generic_context(db); assert_eq!(other.generic_context(db), generic_context); // TODO special-casing Unknown to mean "no mapping" is not right here, and can give @@ -1392,7 +1384,7 @@ impl<'db> Specialization<'db> { Specialization::new(db, self.generic_context(db), types, None, None) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -1426,7 +1418,7 @@ impl<'db> Specialization<'db> { )) } - pub(super) fn materialize_impl( + pub fn materialize_impl( self, db: &'db dyn Db, materialization_kind: MaterializationKind, @@ -1490,7 +1482,7 @@ impl<'db> Specialization<'db> { } #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -1570,7 +1562,7 @@ impl<'db> Specialization<'db> { }) } - pub(crate) fn is_disjoint_from<'c>( + pub fn is_disjoint_from<'c>( self, db: &'db dyn Db, other: Self, @@ -1587,7 +1579,7 @@ impl<'db> Specialization<'db> { ) } - pub(crate) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -1650,7 +1642,7 @@ impl<'db> Specialization<'db> { ) } - pub(crate) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -1685,7 +1677,7 @@ pub enum ApplySpecialization<'a, 'db> { impl<'db> ApplySpecialization<'_, 'db> { /// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this /// mapping. - pub(crate) fn get( + pub fn get( &self, db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>, @@ -1716,7 +1708,7 @@ impl<'db> ApplySpecialization<'_, 'db> { /// Performs type inference between parameter annotations and argument types, producing a /// specialization of a generic function. -pub(crate) struct SpecializationBuilder<'db> { +pub struct SpecializationBuilder<'db> { db: &'db dyn Db, inferable: InferableTypeVars<'db, 'db>, types: FxHashMap, Type<'db>>, @@ -1724,10 +1716,10 @@ pub(crate) struct SpecializationBuilder<'db> { /// An assignment from a bound type variable to a given type, along with the variance of the outermost /// type with respect to the type variable. -pub(crate) type TypeVarAssignment<'db> = (BoundTypeVarIdentity<'db>, TypeVarVariance, Type<'db>); +pub type TypeVarAssignment<'db> = (BoundTypeVarIdentity<'db>, TypeVarVariance, Type<'db>); impl<'db> SpecializationBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db, inferable: InferableTypeVars<'db, 'db>) -> Self { + pub fn new(db: &'db dyn Db, inferable: InferableTypeVars<'db, 'db>) -> Self { Self { db, inferable, @@ -1736,17 +1728,17 @@ impl<'db> SpecializationBuilder<'db> { } /// Returns the current set of type mappings for this specialization. - pub(crate) fn type_mappings(&self) -> &FxHashMap, Type<'db>> { + pub fn type_mappings(&self) -> &FxHashMap, Type<'db>> { &self.types } /// Returns the current set of type mappings for this specialization. - pub(crate) fn into_type_mappings(self) -> FxHashMap, Type<'db>> { + pub fn into_type_mappings(self) -> FxHashMap, Type<'db>> { self.types } /// Map the types that have been assigned in this specialization. - pub(crate) fn mapped( + pub fn mapped( &self, generic_context: GenericContext<'db>, f: impl Fn(BoundTypeVarInstance<'db>, Type<'db>) -> Type<'db>, @@ -1765,7 +1757,7 @@ impl<'db> SpecializationBuilder<'db> { } } - pub(crate) fn with_default( + pub fn with_default( &self, generic_context: GenericContext<'db>, default_ty: impl Fn(BoundTypeVarInstance<'db>) -> Type<'db>, @@ -1784,7 +1776,7 @@ impl<'db> SpecializationBuilder<'db> { } } - pub(crate) fn build(&mut self, generic_context: GenericContext<'db>) -> Specialization<'db> { + pub fn build(&mut self, generic_context: GenericContext<'db>) -> Specialization<'db> { let types = generic_context .variables_inner(self.db) .iter() @@ -1909,7 +1901,7 @@ impl<'db> SpecializationBuilder<'db> { } /// Infer type mappings for the specialization based on a given type and its declared type. - pub(crate) fn infer( + pub fn infer( &mut self, constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, @@ -1922,7 +1914,7 @@ impl<'db> SpecializationBuilder<'db> { /// /// The provided function will be called before any type mappings are created, and can /// optionally modify the inferred type, or filter out the type mapping entirely. - pub(crate) fn infer_map( + pub fn infer_map( &mut self, constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, @@ -2451,7 +2443,7 @@ impl<'db> SpecializationBuilder<'db> { /// Infer type mappings for the specialization in the reverse direction, i.e., where the /// actual type, not the formal type, contains inferable type variables. - pub(crate) fn infer_reverse( + pub fn infer_reverse( &mut self, constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, @@ -2465,7 +2457,7 @@ impl<'db> SpecializationBuilder<'db> { /// /// The provided function will be called before any type mappings are created, and can /// optionally modify the inferred type, or filter out the type mapping entirely. - pub(crate) fn infer_reverse_map( + pub fn infer_reverse_map( &mut self, constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, @@ -2557,7 +2549,7 @@ impl<'db> SpecializationBuilder<'db> { } #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum SpecializationError<'db> { +pub enum SpecializationError<'db> { MismatchedBound { bound_typevar: BoundTypeVarInstance<'db>, argument: Type<'db>, @@ -2569,14 +2561,14 @@ pub(crate) enum SpecializationError<'db> { } impl<'db> SpecializationError<'db> { - pub(crate) fn bound_typevar(&self) -> BoundTypeVarInstance<'db> { + pub fn bound_typevar(&self) -> BoundTypeVarInstance<'db> { match self { Self::MismatchedBound { bound_typevar, .. } => *bound_typevar, Self::MismatchedConstraint { bound_typevar, .. } => *bound_typevar, } } - pub(crate) fn argument_type(&self) -> Type<'db> { + pub fn argument_type(&self) -> Type<'db> { match self { Self::MismatchedBound { argument, .. } => *argument, Self::MismatchedConstraint { argument, .. } => *argument, diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 5eb5e6249789f..3a588ba924010 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1097,7 +1097,7 @@ mod resolve_definition { /// Returns resolved definitions which can be either specific definitions or module files. /// For non-import definitions, returns the definition wrapped in `ResolvedDefinition::Definition`. /// Always returns at least the original definition as a fallback if resolution fails. - pub(crate) fn resolve_definition<'db>( + pub fn resolve_definition<'db>( db: &'db dyn Db, definition: Definition<'db>, symbol_name: Option<&str>, @@ -1219,7 +1219,7 @@ mod resolve_definition { } /// Helper function to resolve import definitions for `ImportFrom` and `StarImport` cases. - pub(crate) fn resolve_from_import_definitions<'db>( + pub fn resolve_from_import_definitions<'db>( db: &'db dyn Db, file: File, import_node: &ast::StmtImportFrom, @@ -1303,7 +1303,7 @@ mod resolve_definition { } /// Find definitions for a symbol name in a specific scope. - pub(crate) fn find_symbol_in_scope<'db>( + pub fn find_symbol_in_scope<'db>( db: &'db dyn Db, scope: ScopeId<'db>, symbol_name: &str, diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 185b83d98ebcf..458ea2863026f 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -57,7 +57,7 @@ use crate::types::{ }; use crate::unpack::Unpack; use builder::TypeInferenceBuilder; -pub(super) use comparisons::UnsupportedComparisonError; +pub use comparisons::UnsupportedComparisonError; mod builder; mod comparisons; @@ -75,7 +75,7 @@ mod tests; }, heap_size=ruff_memory_usage::heap_size )] -pub(crate) fn infer_definition_types<'db>( +pub fn infer_definition_types<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { @@ -114,7 +114,7 @@ fn definition_cycle_initial<'db>( }, heap_size=ruff_memory_usage::heap_size )] -pub(crate) fn infer_deferred_types<'db>( +pub fn infer_deferred_types<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { @@ -148,7 +148,7 @@ fn deferred_cycle_initial<'db>( /// /// Unlike [`infer_scope_types`], this function does not take a type context, as it may infer /// the parent scope to obtain the necessary type context by which to infer the inner scope. -pub(crate) fn infer_complete_scope_types<'db>( +pub fn infer_complete_scope_types<'db>( db: &'db dyn Db, scope: ScopeId<'db>, ) -> &'db ScopeInference<'db> { @@ -176,7 +176,7 @@ pub(crate) fn infer_complete_scope_types<'db>( /// unless you have already obtained the necessary type context while inferring the parent scope. /// Inferring a nested scope independently without type context can lead to incorrect inferred /// types or diagnostics. -pub(crate) fn infer_scope_types<'db>( +pub fn infer_scope_types<'db>( db: &'db dyn Db, scope: ScopeId<'db>, tcx: TypeContext<'db>, @@ -192,10 +192,7 @@ pub(crate) fn infer_scope_types<'db>( }, heap_size=ruff_memory_usage::heap_size )] -pub(crate) fn infer_scope_types_impl<'db>( - db: &'db dyn Db, - input: InferScope<'db>, -) -> ScopeInference<'db> { +pub fn infer_scope_types_impl<'db>(db: &'db dyn Db, input: InferScope<'db>) -> ScopeInference<'db> { let (scope, tcx) = input.into_inner(db); let file = scope.file(db); let _span = tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?file).entered(); @@ -213,7 +210,7 @@ pub(crate) fn infer_scope_types_impl<'db>( /// Use rarely; only for cases where we'd otherwise risk double-inferring an expression: RHS of an /// assignment, which might be unpacking/multi-target and thus part of multiple definitions, or a /// type narrowing guard expression (e.g. if statement test node). -pub(crate) fn infer_expression_types<'db>( +pub fn infer_expression_types<'db>( db: &'db dyn Db, expression: Expression<'db>, tcx: TypeContext<'db>, @@ -229,7 +226,7 @@ pub(crate) fn infer_expression_types<'db>( }, heap_size=ruff_memory_usage::heap_size )] -pub(super) fn infer_expression_types_impl<'db>( +pub fn infer_expression_types_impl<'db>( db: &'db dyn Db, input: InferExpression<'db>, ) -> ExpressionInference<'db> { @@ -271,7 +268,7 @@ fn expression_cycle_initial<'db>( /// This is a small helper around [`infer_expression_types()`] to reduce the boilerplate. /// Use [`infer_expression_type()`] if it isn't guaranteed that `expression` is in the same file to /// avoid cross-file query dependencies. -pub(crate) fn infer_same_file_expression_type<'db>( +pub fn infer_same_file_expression_type<'db>( db: &'db dyn Db, expression: Expression<'db>, tcx: TypeContext<'db>, @@ -288,7 +285,7 @@ pub(crate) fn infer_same_file_expression_type<'db>( /// /// Use [`infer_same_file_expression_type`] if it is guaranteed that `expression` is in the same /// to avoid unnecessary salsa ingredients. This is normally the case inside the `TypeInferenceBuilder`. -pub(crate) fn infer_expression_type<'db>( +pub fn infer_expression_type<'db>( db: &'db dyn Db, expression: Expression<'db>, tcx: TypeContext<'db>, @@ -320,19 +317,19 @@ fn infer_expression_type_impl<'db>(db: &'db dyn Db, input: InferExpression<'db>) /// This is a Salsa supertype used as the input to `infer_expression_types` to avoid /// interning an `ExpressionWithContext` unnecessarily when no type context is provided. #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, salsa::Supertype, salsa::Update)] -pub(super) enum InferExpression<'db> { +pub enum InferExpression<'db> { Bare(Expression<'db>), WithContext(ExpressionWithContext<'db>), } #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub(super) struct ExpressionWithContext<'db> { +pub struct ExpressionWithContext<'db> { expression: Expression<'db>, tcx: TypeContext<'db>, } impl<'db> InferExpression<'db> { - pub(super) fn new( + pub fn new( db: &'db dyn Db, expression: Expression<'db>, tcx: TypeContext<'db>, @@ -357,23 +354,19 @@ impl<'db> InferExpression<'db> { /// A `ScopeId` with an optional `TypeContext`. #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, salsa::Supertype, salsa::Update)] -pub(super) enum InferScope<'db> { +pub enum InferScope<'db> { Bare(ScopeId<'db>), WithContext(ScopeWithContext<'db>), } #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub(super) struct ScopeWithContext<'db> { +pub struct ScopeWithContext<'db> { scope: ScopeId<'db>, tcx: TypeContext<'db>, } impl<'db> InferScope<'db> { - pub(super) fn new( - db: &'db dyn Db, - scope: ScopeId<'db>, - tcx: TypeContext<'db>, - ) -> InferScope<'db> { + pub fn new(db: &'db dyn Db, scope: ScopeId<'db>, tcx: TypeContext<'db>) -> InferScope<'db> { if tcx.annotation.is_some() { InferScope::WithContext(ScopeWithContext::new(db, scope, tcx)) } else { @@ -397,12 +390,12 @@ impl<'db> InferScope<'db> { /// Knowing the outer type context when inferring an expression can enable /// more precise inference results, aka "bidirectional type inference". #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)] -pub(crate) struct TypeContext<'db> { - pub(crate) annotation: Option>, +pub struct TypeContext<'db> { + pub annotation: Option>, } impl<'db> TypeContext<'db> { - pub(crate) fn new(annotation: Option>) -> Self { + pub fn new(annotation: Option>) -> Self { Self { annotation } } @@ -417,13 +410,13 @@ impl<'db> TypeContext<'db> { .and_then(|ty| ty.known_specialization(db, known_class)) } - pub(crate) fn map(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { + pub fn map(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { Self { annotation: self.annotation.map(f), } } - pub(crate) fn is_typealias(&self) -> bool { + pub fn is_typealias(&self) -> bool { self.annotation .is_some_and(|ty| ty.is_typealias_special_form()) } @@ -443,7 +436,7 @@ impl<'db> TypeContext<'db> { }, heap_size=ruff_memory_usage::heap_size )] -pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> { +pub fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> { let file = unpack.file(db); let module = parsed_module(db, file).load(db); let _span = tracing::trace_span!("infer_unpack_types", range=?unpack.range(db, &module), ?file) @@ -463,7 +456,7 @@ pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> U /// scope is a type-parameters scope and the grandparent scope is a class. /// /// Returns `None` if no enclosing class is found. -pub(crate) fn nearest_enclosing_class<'db>( +pub fn nearest_enclosing_class<'db>( db: &'db dyn Db, semantic: &SemanticIndex<'db>, scope: ScopeId, @@ -486,7 +479,7 @@ pub(crate) fn nearest_enclosing_class<'db>( /// and finds the closest (non-lambda) function definition. /// /// Returns `None` if no enclosing function is found. -pub(crate) fn nearest_enclosing_function<'db>( +pub fn nearest_enclosing_function<'db>( db: &'db dyn Db, semantic: &SemanticIndex<'db>, scope: ScopeId, @@ -506,7 +499,7 @@ pub(crate) fn nearest_enclosing_function<'db>( /// A region within which we can infer types. #[derive(Copy, Clone, Debug)] -pub(crate) enum InferenceRegion<'db> { +pub enum InferenceRegion<'db> { /// infer types for a standalone [`Expression`] Expression(Expression<'db>, TypeContext<'db>), /// infer types for a [`Definition`] @@ -531,7 +524,7 @@ impl<'db> InferenceRegion<'db> { /// The inferred types for a scope region. #[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)] -pub(crate) struct ScopeInference<'db> { +pub struct ScopeInference<'db> { /// The types of every expression in this region. expressions: FxHashMap>, @@ -576,16 +569,16 @@ impl<'db> ScopeInference<'db> { self } - pub(crate) fn diagnostics(&self) -> Option<&TypeCheckDiagnostics> { + pub fn diagnostics(&self) -> Option<&TypeCheckDiagnostics> { self.extra.as_deref().map(|extra| &extra.diagnostics) } - pub(crate) fn expression_type(&self, expression: impl Into) -> Type<'db> { + pub fn expression_type(&self, expression: impl Into) -> Type<'db> { self.try_expression_type(expression) .unwrap_or_else(Type::unknown) } - pub(crate) fn try_expression_type( + pub fn try_expression_type( &self, expression: impl Into, ) -> Option> { @@ -601,7 +594,7 @@ impl<'db> ScopeInference<'db> { /// Returns whether the given expression is a string annotation /// (the string in `x: "int | None"`). - pub(crate) fn is_string_annotation(&self, expression: impl Into) -> bool { + pub fn is_string_annotation(&self, expression: impl Into) -> bool { let Some(extra) = &self.extra else { return false; }; @@ -612,7 +605,7 @@ impl<'db> ScopeInference<'db> { /// The inferred types for a definition region. #[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)] -pub(crate) struct DefinitionInference<'db> { +pub struct DefinitionInference<'db> { /// The types of every expression in this region. expressions: FxHashMap>, @@ -624,7 +617,7 @@ pub(crate) struct DefinitionInference<'db> { /// /// Almost all definition regions have less than 10 bindings. There are very few with more than 10 (but still less than 20). /// Because of that, use a slice with linear search over a hash map. - pub(crate) bindings: Box<[(Definition<'db>, Type<'db>)]>, + pub bindings: Box<[(Definition<'db>, Type<'db>)]>, /// The types and type qualifiers of every declaration in this region. /// @@ -714,12 +707,12 @@ impl<'db> DefinitionInference<'db> { self } - pub(crate) fn expression_type(&self, expression: impl Into) -> Type<'db> { + pub fn expression_type(&self, expression: impl Into) -> Type<'db> { self.try_expression_type(expression) .unwrap_or_else(Type::unknown) } - pub(crate) fn try_expression_type( + pub fn try_expression_type( &self, expression: impl Into, ) -> Option> { @@ -730,7 +723,7 @@ impl<'db> DefinitionInference<'db> { } #[track_caller] - pub(crate) fn binding_type(&self, definition: Definition<'db>) -> Type<'db> { + pub fn binding_type(&self, definition: Definition<'db>) -> Type<'db> { self.bindings .iter() .find_map( @@ -750,7 +743,7 @@ impl<'db> DefinitionInference<'db> { } #[track_caller] - pub(crate) fn declaration_type(&self, definition: Definition<'db>) -> TypeAndQualifiers<'db> { + pub fn declaration_type(&self, definition: Definition<'db>) -> TypeAndQualifiers<'db> { self.declarations .iter() .find_map(|(def, qualifiers)| { @@ -777,18 +770,18 @@ impl<'db> DefinitionInference<'db> { self.declarations.iter().map(|(_, qualifiers)| *qualifiers) } - pub(crate) fn fallback_type(&self) -> Option> { + pub fn fallback_type(&self) -> Option> { self.extra.as_ref().and_then(|extra| extra.cycle_recovery) } - pub(crate) fn undecorated_type(&self) -> Option> { + pub fn undecorated_type(&self) -> Option> { self.extra.as_ref().and_then(|extra| extra.undecorated_type) } } /// The inferred types for an expression region. #[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)] -pub(crate) struct ExpressionInference<'db> { +pub struct ExpressionInference<'db> { /// The types of every expression in this region. expressions: FxHashMap>, @@ -864,7 +857,7 @@ impl<'db> ExpressionInference<'db> { self } - pub(crate) fn try_expression_type( + pub fn try_expression_type( &self, expression: impl Into, ) -> Option> { @@ -874,7 +867,7 @@ impl<'db> ExpressionInference<'db> { .or_else(|| self.fallback_type()) } - pub(crate) fn expression_type(&self, expression: impl Into) -> Type<'db> { + pub fn expression_type(&self, expression: impl Into) -> Type<'db> { self.try_expression_type(expression) .unwrap_or_else(Type::unknown) } @@ -886,7 +879,7 @@ impl<'db> ExpressionInference<'db> { bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub(crate) struct InferenceFlags: u8 { + pub struct InferenceFlags: u8 { /// Whether to allow `ParamSpec` in type expressions. /// /// In most contexts inside type expressions, bare `ParamSpec`s are not allowed. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b57c6867c7fc2..f2e9146024ed8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -205,7 +205,7 @@ const NUM_FIELD_SPECIFIERS_INLINE: usize = 1; /// Similarly, when we encounter a standalone-inferable expression (right-hand side of an /// assignment, type narrowing guard), we use the [`infer_expression_types()`] query to ensure we /// don't infer its types more than once. -pub(super) struct TypeInferenceBuilder<'db, 'ast> { +pub struct TypeInferenceBuilder<'db, 'ast> { context: InferContext<'db, 'ast>, index: &'db SemanticIndex<'db>, @@ -310,10 +310,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// /// This is a fairly arbitrary number. It should be *far* more than enough /// for most use cases, but we can reevaluate it later if useful. - pub(super) const MAX_STRING_LITERAL_SIZE: usize = 4096; + pub const MAX_STRING_LITERAL_SIZE: usize = 4096; /// Creates a new builder for inferring types in a region. - pub(super) fn new( + pub fn new( db: &'db dyn Db, region: InferenceRegion<'db>, index: &'db SemanticIndex<'db>, @@ -7776,7 +7776,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (place, constraint_keys) } - pub(super) fn report_unresolved_reference(&self, expr_name_node: &ast::ExprName) { + pub fn report_unresolved_reference(&self, expr_name_node: &ast::ExprName) { if !self.is_reachable(expr_name_node) { return; } @@ -8605,7 +8605,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn finish_expression(mut self) -> ExpressionInference<'db> { + pub fn finish_expression(mut self) -> ExpressionInference<'db> { self.infer_region(); let Self { @@ -8677,7 +8677,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn finish_definition(mut self) -> DefinitionInference<'db> { + pub fn finish_definition(mut self) -> DefinitionInference<'db> { self.infer_region(); let Self { @@ -8756,7 +8756,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn finish_scope(mut self) -> ScopeInference<'db> { + pub fn finish_scope(mut self) -> ScopeInference<'db> { self.infer_region(); let Self { @@ -9338,7 +9338,7 @@ enum BoundOrConstraintsNodes<'ast> { /// Report MRO errors for a dynamic class. /// /// Returns `true` if the MRO is valid, `false` if there were errors. -pub(super) fn report_dynamic_mro_errors<'db>( +pub fn report_dynamic_mro_errors<'db>( context: &InferContext<'db, '_>, dynamic_class: DynamicClassLiteral<'db>, call_expr: &ast::ExprCall, diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 76d8f83ccd297..bb2b8f7dda90d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -24,7 +24,7 @@ enum PEP613Policy { /// Annotation expressions. impl<'db> TypeInferenceBuilder<'db, '_> { /// Infer the type of an annotation expression with the given [`DeferredExpressionState`]. - pub(super) fn infer_annotation_expression( + pub fn infer_annotation_expression( &mut self, annotation: &ast::Expr, deferred_state: DeferredExpressionState, @@ -34,7 +34,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// Infer the type of an annotation expression with the given [`DeferredExpressionState`], /// allowing a PEP 613 `typing.TypeAlias` annotation. - pub(super) fn infer_annotation_expression_allow_pep_613( + pub fn infer_annotation_expression_allow_pep_613( &mut self, annotation: &ast::Expr, deferred_state: DeferredExpressionState, @@ -46,7 +46,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// and returns [`None`] if the annotation is [`None`]. /// /// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression - pub(super) fn infer_optional_annotation_expression( + pub fn infer_optional_annotation_expression( &mut self, annotation: Option<&ast::Expr>, deferred_state: DeferredExpressionState, diff --git a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs index f56cf80b398fb..d58759598e403 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs @@ -15,7 +15,7 @@ use ruff_python_ast::PythonVersion; use crate::Program; impl<'db> TypeInferenceBuilder<'db, '_> { - pub(super) fn infer_binary_expression( + pub fn infer_binary_expression( &mut self, binary: &ast::ExprBinOp, tcx: TypeContext<'db>, @@ -87,7 +87,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// Returns the original `TypeVar` if each result is equivalent to its input constraint; /// otherwise returns the union of all results. - pub(super) fn map_constrained_typevar_constraints( + pub fn map_constrained_typevar_constraints( db: &'db dyn Db, typevar: Type<'db>, constraints: TypeVarConstraints<'db>, @@ -111,7 +111,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }) } - pub(super) fn infer_binary_expression_type( + pub fn infer_binary_expression_type( &mut self, node: AnyNodeRef<'_>, mut emitted_division_by_zero_diagnostic: bool, diff --git a/crates/ty_python_semantic/src/types/infer/builder/class.rs b/crates/ty_python_semantic/src/types/infer/builder/class.rs index 824ef6ce331fc..971e2de9ee002 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/class.rs @@ -16,11 +16,11 @@ use ruff_python_ast::{self as ast, helpers::any_over_expr}; use ty_module_resolver::{KnownModule, file_to_module}; impl<'db> TypeInferenceBuilder<'db, '_> { - pub(super) fn infer_class_body(&mut self, class: &ast::StmtClassDef) { + pub fn infer_class_body(&mut self, class: &ast::StmtClassDef) { self.infer_body(&class.body); } - pub(super) fn infer_class_type_params(&mut self, class: &ast::StmtClassDef) { + pub fn infer_class_type_params(&mut self, class: &ast::StmtClassDef) { let type_params = class .type_params .as_deref() @@ -52,11 +52,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.typevar_binding_context = previous_typevar_binding_context; } - pub(super) fn infer_class_definition_statement(&mut self, class: &ast::StmtClassDef) { + pub fn infer_class_definition_statement(&mut self, class: &ast::StmtClassDef) { self.infer_definition(class); } - pub(super) fn infer_class_definition( + pub fn infer_class_definition( &mut self, class_node: &ast::StmtClassDef, definition: Definition<'db>, @@ -235,11 +235,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - pub(super) fn infer_class_deferred( - &mut self, - definition: Definition<'db>, - class: &ast::StmtClassDef, - ) { + pub fn infer_class_deferred(&mut self, definition: Definition<'db>, class: &ast::StmtClassDef) { let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); for base in class.bases() { if self.in_stub() { diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index 090939036a98b..1f0d0bf981979 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -36,7 +36,7 @@ use ruff_python_ast as ast; use ruff_text_size::Ranged; impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { - pub(super) fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) { + pub fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) { let db = self.db(); // Parameters are odd: they are Definitions in the function body scope, but have no @@ -160,11 +160,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn infer_function_definition_statement(&mut self, function: &ast::StmtFunctionDef) { + pub fn infer_function_definition_statement(&mut self, function: &ast::StmtFunctionDef) { self.infer_definition(function); } - pub(super) fn infer_function_definition( + pub fn infer_function_definition( &mut self, function: &ast::StmtFunctionDef, definition: Definition<'db>, @@ -346,7 +346,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn infer_function_deferred( + pub fn infer_function_deferred( &mut self, definition: Definition<'db>, function: &ast::StmtFunctionDef, @@ -464,7 +464,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn infer_function_type_params(&mut self, function: &ast::StmtFunctionDef) { + pub fn infer_function_type_params(&mut self, function: &ast::StmtFunctionDef) { let type_params = function .type_params .as_deref() @@ -581,7 +581,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// It is safe (non-cycle-causing) to query the annotation type via `file_expression_type` /// here, because an outer scope can't depend on a definition from an inner scope, so we /// shouldn't be in-process of inferring the outer scope here. - pub(super) fn infer_parameter_definition( + pub fn infer_parameter_definition( &mut self, parameter_with_default: &'ast ast::ParameterWithDefault, definition: Definition<'db>, @@ -680,7 +680,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. /// /// [`infer_parameter_definition`]: Self::infer_parameter_definition - pub(super) fn infer_variadic_positional_parameter_definition( + pub fn infer_variadic_positional_parameter_definition( &mut self, parameter: &'ast ast::Parameter, definition: Definition<'db>, @@ -820,7 +820,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. /// /// [`infer_parameter_definition`]: Self::infer_parameter_definition - pub(super) fn infer_variadic_keyword_parameter_definition( + pub fn infer_variadic_keyword_parameter_definition( &mut self, parameter: &'ast ast::Parameter, definition: Definition<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder/imports.rs b/crates/ty_python_semantic/src/types/infer/builder/imports.rs index 0bfef1d10a004..9debef9131b79 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/imports.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/imports.rs @@ -21,7 +21,7 @@ use crate::{ }; impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { - pub(super) fn infer_import_statement(&mut self, import: &ast::StmtImport) { + pub fn infer_import_statement(&mut self, import: &ast::StmtImport) { let ast::StmtImport { names, is_lazy: _, @@ -157,7 +157,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - pub(super) fn infer_import_definition( + pub fn infer_import_definition( &mut self, node: &ast::StmtImport, alias: &ast::Alias, @@ -232,7 +232,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - pub(super) fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) { + pub fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) { let ast::StmtImportFrom { module: _, names, @@ -333,7 +333,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn infer_import_from_definition( + pub fn infer_import_from_definition( &mut self, import_from: &ast::StmtImportFrom, alias: &ast::Alias, @@ -555,7 +555,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// That gap between the semantics and implementation are currently the responsibility of the /// code that actually creates these kinds of Definitions (so blindly introducing a local /// is all we need to be doing here). - pub(super) fn infer_import_from_submodule_definition( + pub fn infer_import_from_submodule_definition( &mut self, import_from: &'ast ast::StmtImportFrom, definition: Definition<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs index c503461f8423f..1ecc9cf63c52c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs @@ -24,7 +24,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// This method *does not* call `infer_expression` on the object being called; /// it is assumed that the type for this AST node has already been inferred before this method is called. - pub(super) fn infer_namedtuple_call_expression( + pub fn infer_namedtuple_call_expression( &mut self, call_expr: &ast::ExprCall, definition: Option>, @@ -553,7 +553,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { NamedTupleSpec::known(db, fields) } - pub(super) fn infer_typing_namedtuple_fields( + pub fn infer_typing_namedtuple_fields( &mut self, fields_arg: &ast::Expr, ) -> NamedTupleSpec<'db> { @@ -749,7 +749,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum NamedTupleKind { +pub enum NamedTupleKind { Collections, Typing, } @@ -763,7 +763,7 @@ impl NamedTupleKind { matches!(self, Self::Typing) } - pub(super) fn from_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { + pub fn from_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { match ty { Type::SpecialForm(SpecialFormType::NamedTuple) => Some(NamedTupleKind::Typing), Type::FunctionLiteral(function) => function diff --git a/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs b/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs index dcd2ee3f54d08..29c2bfc9d7a95 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs @@ -9,7 +9,7 @@ use ruff_text_size::Ranged; /// - `P.args` and `P.kwargs` must always be used together /// - When `*args: P.args` is present, `**kwargs: P.kwargs` must also be present (same P) /// - No keyword-only parameters are allowed between `*args: P.args` and `**kwargs: P.kwargs` -pub(super) fn validate_paramspec_components<'db>( +pub fn validate_paramspec_components<'db>( context: &'db InferContext<'db, '_>, parameters: &ast::Parameters, infer_type: impl Fn(&ast::Expr) -> Type<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index faa24be3f93fb..e8424a1ab61b3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -36,10 +36,7 @@ use crate::types::{ use crate::{Db, FxOrderSet}; impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { - pub(super) fn infer_subscript_expression( - &mut self, - subscript: &ast::ExprSubscript, - ) -> Type<'db> { + pub fn infer_subscript_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { let ast::ExprSubscript { value, slice, @@ -71,7 +68,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + pub fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); // If we have an implicit type alias like `MyList = list[T]`, and if `MyList` is being @@ -84,7 +81,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_subscript_load_impl(value_ty, subscript) } - pub(super) fn infer_subscript_load_impl( + pub fn infer_subscript_load_impl( &mut self, value_ty: Type<'db>, subscript: &ast::ExprSubscript, @@ -423,7 +420,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys) } - pub(super) fn infer_explicit_class_specialization( + pub fn infer_explicit_class_specialization( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -445,7 +442,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) } - pub(super) fn infer_explicit_type_alias_type_specialization( + pub fn infer_explicit_type_alias_type_specialization( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -469,7 +466,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) } - pub(super) fn infer_explicit_callable_specialization( + pub fn infer_explicit_callable_specialization( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -492,7 +489,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { result } - pub(super) fn infer_explicit_callable_specialization_impl( + pub fn infer_explicit_callable_specialization_impl( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -998,7 +995,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Err(()) } - pub(super) fn infer_subscript_expression_types( + pub fn infer_subscript_expression_types( &self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -1049,7 +1046,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) } - pub(super) fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { + pub fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { enum SliceArg<'db> { Arg(Type<'db>), Unsupported, @@ -1095,7 +1092,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } /// Validate a subscript assignment of the form `object[key] = rhs_value`. - pub(super) fn validate_subscript_assignment( + pub fn validate_subscript_assignment( &mut self, target: &ast::ExprSubscript, rhs_value: &ast::Expr, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index c661519ee4944..9596ab13ee03f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -24,7 +24,7 @@ use crate::{FxOrderSet, Program, add_inferred_python_version_hint_to_diagnostic} /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { /// Infer the type of a type expression. - pub(super) fn infer_type_expression(&mut self, expression: &ast::Expr) -> Type<'db> { + pub fn infer_type_expression(&mut self, expression: &ast::Expr) -> Type<'db> { if self.inner_expression_inference_state.is_get() { return self.expression_type(expression); } @@ -75,7 +75,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } /// Infer the type of a type expression without storing the result. - pub(super) fn infer_type_expression_no_store(&mut self, expression: &ast::Expr) -> Type<'db> { + pub fn infer_type_expression_no_store(&mut self, expression: &ast::Expr) -> Type<'db> { if self.inner_expression_inference_state.is_get() { return self.expression_type(expression); } @@ -696,7 +696,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - pub(super) fn infer_subscript_type_expression_no_store( + pub fn infer_subscript_type_expression_no_store( &mut self, subscript: &ast::ExprSubscript, slice: &ast::Expr, @@ -713,10 +713,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } /// Infer the type of a string type expression. - pub(super) fn infer_string_type_expression( - &mut self, - string: &ast::ExprStringLiteral, - ) -> Type<'db> { + pub fn infer_string_type_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> { match parse_string_annotation(&self.context, string) { Some(parsed) => { self.string_annotations @@ -737,7 +734,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// This method assumes that a type has already been inferred and stored for the `value` /// of the subscript passed in. - pub(super) fn infer_tuple_type_expression( + pub fn infer_tuple_type_expression( &mut self, tuple: &ast::ExprSubscript, ) -> Option> { @@ -1049,7 +1046,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } /// Infer the type of an explicitly specialized generic type alias (implicit or PEP 613). - pub(crate) fn infer_explicit_type_alias_specialization( + pub fn infer_explicit_type_alias_specialization( &mut self, subscript: &ast::ExprSubscript, mut value_ty: Type<'db>, @@ -1137,7 +1134,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } - pub(super) fn infer_subscript_type_expression( + pub fn infer_subscript_type_expression( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -1474,7 +1471,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } /// Infer the type of a `Callable[...]` type expression. - pub(crate) fn infer_callable_type(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + pub fn infer_callable_type(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { fn inner<'db>( builder: &mut TypeInferenceBuilder<'db, '_>, subscript: &ast::ExprSubscript, @@ -2011,7 +2008,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - pub(crate) fn infer_literal_parameter_type<'param>( + pub fn infer_literal_parameter_type<'param>( &mut self, parameters: &'param ast::Expr, ) -> Result, Vec<&'param ast::Expr>> { @@ -2128,7 +2125,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// It returns `None` if the argument is invalid i.e., not a list of types, parameter /// specification, `typing.Concatenate`, or `...`. - pub(super) fn infer_callable_parameter_types( + pub fn infer_callable_parameter_types( &mut self, parameters: &ast::Expr, ) -> Option> { @@ -2238,7 +2235,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// Returns `Unknown` as a fallback if the type variable is unbound, otherwise returns the /// original type unchanged. - pub(super) fn check_for_unbound_type_variable( + pub fn check_for_unbound_type_variable( &self, expression: &ast::Expr, ty: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs index 06d908365e6b2..1481b886b7080 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs @@ -29,7 +29,7 @@ use ruff_python_ast::{self as ast, PythonVersion}; use ruff_text_size::{Ranged, TextRange}; impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { - pub(super) fn infer_typevar_definition( + pub fn infer_typevar_definition( &mut self, node: &ast::TypeParamTypeVar, definition: Definition<'db>, @@ -79,7 +79,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - pub(super) fn infer_typevar_deferred(&mut self, node: &'ast ast::TypeParamTypeVar) { + pub fn infer_typevar_deferred(&mut self, node: &'ast ast::TypeParamTypeVar) { let ast::TypeParamTypeVar { range: _, node_index: _, @@ -158,7 +158,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } /// Validate that a `TypeVar`'s default is compatible with its bound or constraints. - pub(super) fn validate_typevar_default( + pub fn validate_typevar_default( &mut self, name: Option<&str>, bound_or_constraints: Option>, @@ -526,7 +526,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { true } - pub(super) fn infer_paramspec_definition( + pub fn infer_paramspec_definition( &mut self, node: &ast::TypeParamParamSpec, definition: Definition<'db>, @@ -559,7 +559,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - pub(super) fn infer_paramspec_deferred(&mut self, node: &ast::TypeParamParamSpec) { + pub fn infer_paramspec_deferred(&mut self, node: &ast::TypeParamParamSpec) { let ast::TypeParamParamSpec { range: _, node_index: _, @@ -575,7 +575,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.deferred_state = previous_deferred_state; } - pub(super) fn infer_paramspec_default( + pub fn infer_paramspec_default( &mut self, default_expr: &ast::Expr, paramspec_name: Option<&str>, @@ -641,7 +641,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn infer_typevartuple_definition( + pub fn infer_typevartuple_definition( &mut self, node: &ast::TypeParamTypeVarTuple, definition: Definition<'db>, @@ -661,7 +661,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - pub(super) fn infer_legacy_paramspec( + pub fn infer_legacy_paramspec( &mut self, target: &ast::Expr, call_expr: &ast::ExprCall, @@ -817,7 +817,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ))) } - pub(super) fn infer_legacy_typevar( + pub fn infer_legacy_typevar( &mut self, target: &ast::Expr, call_expr: &ast::ExprCall, diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 45ecc78f989ef..4432e0a9eae0b 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -22,7 +22,7 @@ enum IntersectionOn { } /// A [`CycleDetector`] that is used in [`infer_binary_type_comparison`]. -pub(super) type BinaryComparisonVisitor<'db> = CycleDetector< +pub type BinaryComparisonVisitor<'db> = CycleDetector< ast::CmpOp, (Type<'db>, ast::CmpOp, Type<'db>), Result, UnsupportedComparisonError<'db>>, @@ -104,10 +104,10 @@ impl From for ast::CmpOp { /// this struct. In this case, those would be `Literal["foo"]` /// and `Literal[3]`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct UnsupportedComparisonError<'db> { - pub(crate) op: ast::CmpOp, - pub(crate) left_ty: Type<'db>, - pub(crate) right_ty: Type<'db>, +pub struct UnsupportedComparisonError<'db> { + pub op: ast::CmpOp, + pub left_ty: Type<'db>, + pub right_ty: Type<'db>, } /// Infers the type of a binary comparison (e.g. 'left == right'). See @@ -116,7 +116,7 @@ pub(crate) struct UnsupportedComparisonError<'db> { /// /// If the operation is not supported, return an error (we need upstream context to emit a /// diagnostic). -pub(super) fn infer_binary_type_comparison<'db>( +pub fn infer_binary_type_comparison<'db>( context: &InferContext<'db, '_>, left: Type<'db>, op: ast::CmpOp, diff --git a/crates/ty_python_semantic/src/types/infer/deferred/dynamic_class.rs b/crates/ty_python_semantic/src/types/infer/deferred/dynamic_class.rs index 7c1eb3dddfa92..7eb0f575153b6 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/dynamic_class.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/dynamic_class.rs @@ -15,7 +15,7 @@ use crate::{ /// Iterate over all dynamic class definitions (created using `type()` calls) to check that /// the definition will not cause an exception to be raised at runtime. This needs to be done /// after deferred inference completes, since bases may contain forward references. -pub(crate) fn check_dynamic_class_definition<'db>( +pub fn check_dynamic_class_definition<'db>( context: &InferContext<'db, '_>, definition: Definition<'db>, ) { diff --git a/crates/ty_python_semantic/src/types/infer/deferred/final_variable.rs b/crates/ty_python_semantic/src/types/infer/deferred/final_variable.rs index bb55c283eb3a7..59cb3604db276 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/final_variable.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/final_variable.rs @@ -8,10 +8,7 @@ use crate::{ /// Check for `Final`-qualified declarations in module/function scopes that are never /// assigned a value. Class body scopes are handled separately in /// `check_class_final_without_value`. -pub(crate) fn check_final_without_value<'db>( - context: &InferContext<'db, '_>, - index: &SemanticIndex<'db>, -) { +pub fn check_final_without_value<'db>(context: &InferContext<'db, '_>, index: &SemanticIndex<'db>) { // In stub files, bare declarations without values are normal. if context.in_stub() { return; diff --git a/crates/ty_python_semantic/src/types/infer/deferred/function.rs b/crates/ty_python_semantic/src/types/infer/deferred/function.rs index b7b6edc160d7c..3756b90b7f463 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/function.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/function.rs @@ -19,7 +19,7 @@ use ruff_db::{ use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; -pub(crate) fn check_function_definition<'db>( +pub fn check_function_definition<'db>( context: &InferContext<'db, '_>, definition: Definition<'db>, file_expression_type: &impl Fn(&ast::Expr) -> Type<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/deferred/mod.rs b/crates/ty_python_semantic/src/types/infer/deferred/mod.rs index 09bc583400bdd..8d2ac0cb6ad2b 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/mod.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/mod.rs @@ -1,10 +1,10 @@ //! A home for deferred checks that must be done after the `TypeInferenceBuilder` has done an initial //! inference pass over the whole scope. -pub(super) mod dynamic_class; -pub(super) mod final_variable; -pub(super) mod function; -pub(super) mod overloaded_function; -pub(super) mod static_class; -pub(super) mod type_param_validation; -pub(super) mod typeguard; +pub mod dynamic_class; +pub mod final_variable; +pub mod function; +pub mod overloaded_function; +pub mod static_class; +pub mod type_param_validation; +pub mod typeguard; diff --git a/crates/ty_python_semantic/src/types/infer/deferred/overloaded_function.rs b/crates/ty_python_semantic/src/types/infer/deferred/overloaded_function.rs index 11c043c5a24b5..7a708302e992b 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/overloaded_function.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/overloaded_function.rs @@ -22,7 +22,7 @@ use crate::{ /// /// For (1), this has the consequence of not checking an overloaded function that is being /// shadowed by another function with the same name in this scope. -pub(crate) fn check_overloaded_function<'db>( +pub fn check_overloaded_function<'db>( context: &InferContext<'db, '_>, ty: Type<'db>, definition: Definition<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs b/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs index 8f29e86b9653e..d25bd29d557ea 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/static_class.rs @@ -65,7 +65,7 @@ use crate::{ /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order /// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses -pub(crate) fn check_static_class_definitions<'db>( +pub fn check_static_class_definitions<'db>( context: &InferContext<'db, '_>, ty: Type<'db>, class_node: &ast::StmtClassDef, diff --git a/crates/ty_python_semantic/src/types/infer/deferred/type_param_validation.rs b/crates/ty_python_semantic/src/types/infer/deferred/type_param_validation.rs index 8ed101693a20b..e4210c712182b 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/type_param_validation.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/type_param_validation.rs @@ -9,7 +9,7 @@ use crate::types::{context::InferContext, diagnostic::INVALID_TYPE_VARIABLE_DEFA /// consumes all remaining positional type arguments. /// /// This check is used for both classes and type aliases with PEP 695 type parameters. -pub(crate) fn check_no_default_after_typevar_tuple_pep695( +pub fn check_no_default_after_typevar_tuple_pep695( context: &InferContext<'_, '_>, type_params: &ast::TypeParams, ) { diff --git a/crates/ty_python_semantic/src/types/infer/deferred/typeguard.rs b/crates/ty_python_semantic/src/types/infer/deferred/typeguard.rs index 36d2f92d73432..bee3b56e04b5f 100644 --- a/crates/ty_python_semantic/src/types/infer/deferred/typeguard.rs +++ b/crates/ty_python_semantic/src/types/infer/deferred/typeguard.rs @@ -8,7 +8,7 @@ use crate::{ /// Check that all type guard function definitions have at least one positional parameter /// (in addition to `self`/`cls` for methods), and for `TypeIs`, that the narrowed type is /// assignable to the declared type of that parameter. -pub(crate) fn check_type_guard_definition<'db>( +pub fn check_type_guard_definition<'db>( context: &InferContext<'db, '_>, ty: Type<'db>, node: &ast::StmtFunctionDef, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 48ec1a0a16191..6549560379a87 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -24,21 +24,21 @@ use crate::types::{ LiteralValueTypeKind, TypeContext, TypeMapping, VarianceInferable, }; use crate::{Db, FxOrderSet, Program}; -pub(super) use synthesized_protocol::SynthesizedProtocolType; +pub use synthesized_protocol::SynthesizedProtocolType; impl<'db> Type<'db> { - pub(crate) const fn object() -> Self { + pub const fn object() -> Self { Type::NominalInstance(NominalInstanceType(NominalInstanceInner::Object)) } - pub(crate) const fn is_object(&self) -> bool { + pub const fn is_object(&self) -> bool { matches!( self, Type::NominalInstance(NominalInstanceType(NominalInstanceInner::Object)) ) } - pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { + pub fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { match class.class_literal(db) { // Dynamic classes created via `type()` don't have special instance types. // TODO: When we add functional TypedDict support, this branch should check @@ -78,7 +78,7 @@ impl<'db> Type<'db> { } } - pub(crate) fn tuple(tuple: Option>) -> Self { + pub fn tuple(tuple: Option>) -> Self { let Some(tuple) = tuple else { return Type::Never; }; @@ -89,7 +89,7 @@ impl<'db> Type<'db> { Type::tuple_instance(TupleType::homogeneous(db, element)) } - pub(crate) fn heterogeneous_tuple(db: &'db dyn Db, elements: I) -> Self + pub fn heterogeneous_tuple(db: &'db dyn Db, elements: I) -> Self where I: IntoIterator, T: Into>, @@ -100,7 +100,7 @@ impl<'db> Type<'db> { )) } - pub(crate) fn empty_tuple(db: &'db dyn Db) -> Self { + pub fn empty_tuple(db: &'db dyn Db) -> Self { Type::tuple_instance(TupleType::empty(db)) } @@ -109,11 +109,11 @@ impl<'db> Type<'db> { Type::NominalInstance(NominalInstanceType(NominalInstanceInner::ExactTuple(tuple))) } - pub(crate) const fn is_nominal_instance(self) -> bool { + pub const fn is_nominal_instance(self) -> bool { matches!(self, Type::NominalInstance(_)) } - pub(crate) const fn as_nominal_instance(self) -> Option> { + pub const fn as_nominal_instance(self) -> Option> { match self { Type::NominalInstance(instance_type) => Some(instance_type), _ => None, @@ -121,7 +121,7 @@ impl<'db> Type<'db> { } /// Return `true` if `self` is a nominal instance of the given known class. - pub(crate) fn is_instance_of(self, db: &'db dyn Db, known_class: KnownClass) -> bool { + pub fn is_instance_of(self, db: &'db dyn Db, known_class: KnownClass) -> bool { match self { Type::NominalInstance(instance) => instance.class(db).is_known(db, known_class), _ => false, @@ -129,7 +129,7 @@ impl<'db> Type<'db> { } /// Synthesize a protocol instance type with a given set of read-only property members. - pub(super) fn protocol_with_readonly_members<'a, M>(db: &'db dyn Db, members: M) -> Self + pub fn protocol_with_readonly_members<'a, M>(db: &'db dyn Db, members: M) -> Self where M: IntoIterator)>, { @@ -140,7 +140,7 @@ impl<'db> Type<'db> { /// Return `true` if `self` conforms to the interface described by `protocol`. #[expect(clippy::too_many_arguments)] - pub(super) fn satisfies_protocol<'c>( + pub fn satisfies_protocol<'c>( self, db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, @@ -235,7 +235,7 @@ pub struct NominalInstanceType<'db>( NominalInstanceInner<'db>, ); -pub(super) fn walk_nominal_instance_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_nominal_instance_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, nominal: NominalInstanceType<'db>, visitor: &V, @@ -278,7 +278,7 @@ impl<'db> NominalInstanceType<'db> { file_to_module(db, file).map(|module| module.name(db)) } - pub(super) fn class(&self, db: &'db dyn Db) -> ClassType<'db> { + pub fn class(&self, db: &'db dyn Db) -> ClassType<'db> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db), NominalInstanceInner::NonTuple(class) => class, @@ -287,13 +287,13 @@ impl<'db> NominalInstanceType<'db> { } /// Returns the class literal for this instance. - pub(super) fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> { + pub fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> { self.class(db).class_literal(db) } /// Returns the [`KnownClass`] that this is a nominal instance of, or `None` if it is not an /// instance of a known class. - pub(super) fn known_class(&self, db: &'db dyn Db) -> Option { + pub fn known_class(&self, db: &'db dyn Db) -> Option { match self.0 { NominalInstanceInner::ExactTuple(_) => Some(KnownClass::Tuple), NominalInstanceInner::NonTuple(class) => class.known(db), @@ -302,7 +302,7 @@ impl<'db> NominalInstanceType<'db> { } /// Returns whether this is a nominal instance of a particular [`KnownClass`]. - pub(super) fn has_known_class(&self, db: &'db dyn Db, known_class: KnownClass) -> bool { + pub fn has_known_class(&self, db: &'db dyn Db, known_class: KnownClass) -> bool { self.known_class(db) == Some(known_class) } @@ -310,7 +310,7 @@ impl<'db> NominalInstanceType<'db> { /// /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. - pub(super) fn tuple_spec(&self, db: &'db dyn Db) -> Option>> { + pub fn tuple_spec(&self, db: &'db dyn Db) -> Option>> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))), NominalInstanceInner::NonTuple(class) => { @@ -350,11 +350,11 @@ impl<'db> NominalInstanceType<'db> { } /// Return `true` if this type represents instances of the class `builtins.object`. - pub(super) const fn is_object(self) -> bool { + pub const fn is_object(self) -> bool { matches!(self.0, NominalInstanceInner::Object) } - pub(super) fn is_definition_generic(self) -> bool { + pub fn is_definition_generic(self) -> bool { match self.0 { NominalInstanceInner::NonTuple(class) => class.is_generic(), NominalInstanceInner::ExactTuple(_) => true, @@ -372,7 +372,7 @@ impl<'db> NominalInstanceType<'db> { /// /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. /// But for a subclass of `tuple[int, str]`, it will return `None`. - pub(super) fn own_tuple_spec(&self, db: &'db dyn Db) -> Option>> { + pub fn own_tuple_spec(&self, db: &'db dyn Db) -> Option>> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))), NominalInstanceInner::NonTuple(_) | NominalInstanceInner::Object => None, @@ -384,7 +384,7 @@ impl<'db> NominalInstanceType<'db> { /// /// The specialization must be one in which the typevars are solved as being statically known /// integers or `None`. - pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option { + pub fn slice_literal(self, db: &'db dyn Db) -> Option { let class = match self.0 { NominalInstanceInner::ExactTuple(_) | NominalInstanceInner::Object => return None, NominalInstanceInner::NonTuple(class) => class, @@ -418,7 +418,7 @@ impl<'db> NominalInstanceType<'db> { }) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -438,7 +438,7 @@ impl<'db> NominalInstanceType<'db> { } #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -474,7 +474,7 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -516,7 +516,7 @@ impl<'db> NominalInstanceType<'db> { }) } - pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool { + pub fn is_singleton(self, db: &'db dyn Db) -> bool { match self.0 { // The empty tuple is a singleton on CPython and PyPy, but not on other Python // implementations such as GraalPy. Its *use* as a singleton is discouraged and @@ -531,7 +531,7 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool { + pub fn is_single_valued(self, db: &'db dyn Db) -> bool { match self.0 { NominalInstanceInner::ExactTuple(tuple) => tuple.is_single_valued(db), NominalInstanceInner::Object => false, @@ -543,11 +543,11 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { SubclassOfType::from(db, self.class(db)) } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -567,7 +567,7 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -616,10 +616,10 @@ enum NominalInstanceInner<'db> { NonTuple(ClassType<'db>), } -pub(crate) struct SliceLiteral { - pub(crate) start: Option, - pub(crate) stop: Option, - pub(crate) step: Option, +pub struct SliceLiteral { + pub start: Option, + pub stop: Option, + pub step: Option, } impl<'db> VarianceInferable<'db> for NominalInstanceType<'db> { @@ -632,7 +632,7 @@ impl<'db> VarianceInferable<'db> for NominalInstanceType<'db> { /// that conform to the interface described by a certain protocol. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] pub struct ProtocolInstanceType<'db> { - pub(super) inner: Protocol<'db>, + pub inner: Protocol<'db>, // Keep the inner field here private, // so that the only way of constructing `ProtocolInstanceType` instances @@ -640,7 +640,7 @@ pub struct ProtocolInstanceType<'db> { _phantom: PhantomData<()>, } -pub(super) fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, visitor: &V, @@ -685,7 +685,7 @@ impl<'db> ProtocolInstanceType<'db> { /// If this is a synthesized protocol that does not correspond to a class definition /// in source code, return `None`. These are "pure" abstract types, that cannot be /// treated in a nominal way. - pub(super) fn to_nominal_instance(self) -> Option> { + pub fn to_nominal_instance(self) -> Option> { match self.inner { Protocol::FromClass(class) => { Some(NominalInstanceType(NominalInstanceInner::NonTuple(*class))) @@ -695,7 +695,7 @@ impl<'db> ProtocolInstanceType<'db> { } /// Return the meta-type of this protocol-instance type. - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { match self.inner { Protocol::FromClass(class) => SubclassOfType::from(db, class), @@ -722,7 +722,7 @@ impl<'db> ProtocolInstanceType<'db> { /// as `object` (since `object` is the universal set of *all* possible runtime objects!). /// Such a protocol is therefore an equivalent type to `object`, which would in fact be /// normalised to `object`. - pub(super) fn is_equivalent_to_object(self, db: &'db dyn Db) -> bool { + pub fn is_equivalent_to_object(self, db: &'db dyn Db) -> bool { #[salsa::tracked(cycle_initial=|_, _, _, ()| true, heap_size=ruff_memory_usage::heap_size)] fn is_equivalent_to_object_inner<'db>( db: &'db dyn Db, @@ -746,7 +746,7 @@ impl<'db> ProtocolInstanceType<'db> { is_equivalent_to_object_inner(db, self, ()) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -763,7 +763,7 @@ impl<'db> ProtocolInstanceType<'db> { /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y` /// have a member with the same name but disjoint types #[expect(clippy::unused_self)] - pub(super) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( self, _db: &'db dyn Db, _other: Self, @@ -774,14 +774,14 @@ impl<'db> ProtocolInstanceType<'db> { ConstraintSet::from_bool(constraints, false) } - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { match self.inner { Protocol::FromClass(class) => class.instance_member(db, name), Protocol::Synthesized(synthesized) => synthesized.interface().instance_member(db, name), } } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -798,7 +798,7 @@ impl<'db> ProtocolInstanceType<'db> { } } - pub(super) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -815,7 +815,7 @@ impl<'db> ProtocolInstanceType<'db> { } } - pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + pub fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { self.inner.interface(db) } } @@ -829,7 +829,7 @@ impl<'db> VarianceInferable<'db> for ProtocolInstanceType<'db> { /// An enumeration of the two kinds of protocol types: those that originate from a class /// definition in source code, and those that are synthesized from a set of members. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] -pub(super) enum Protocol<'db> { +pub enum Protocol<'db> { FromClass(ProtocolClass<'db>), Synthesized(SynthesizedProtocolType<'db>), } @@ -871,7 +871,7 @@ impl<'db> VarianceInferable<'db> for Protocol<'db> { } } -mod synthesized_protocol { +pub mod synthesized_protocol { use crate::semantic_index::definition::Definition; use crate::types::protocol_class::ProtocolInterface; use crate::types::{ @@ -882,14 +882,14 @@ mod synthesized_protocol { /// A "synthesized" protocol type that is dissociated from a class definition in source code. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] - pub(in crate::types) struct SynthesizedProtocolType<'db>(ProtocolInterface<'db>); + pub struct SynthesizedProtocolType<'db>(ProtocolInterface<'db>); impl<'db> SynthesizedProtocolType<'db> { - pub(super) fn new(interface: ProtocolInterface<'db>) -> Self { + pub fn new(interface: ProtocolInterface<'db>) -> Self { Self(interface) } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -902,7 +902,7 @@ mod synthesized_protocol { ) } - pub(super) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -913,11 +913,11 @@ mod synthesized_protocol { .find_legacy_typevars_impl(db, binding_context, typevars, visitor); } - pub(in crate::types) fn interface(self) -> ProtocolInterface<'db> { + pub fn interface(self) -> ProtocolInterface<'db> { self.0 } - pub(in crate::types) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/iteration.rs b/crates/ty_python_semantic/src/types/iteration.rs index 3b4ad5d5441a7..c0b3f3989d216 100644 --- a/crates/ty_python_semantic/src/types/iteration.rs +++ b/crates/ty_python_semantic/src/types/iteration.rs @@ -19,7 +19,7 @@ impl<'db> Type<'db> { /// /// This method should only be used outside of type checking because it omits any errors. /// For type checking, use [`try_iterate`](Self::try_iterate) instead. - pub(super) fn iterate(self, db: &'db dyn Db) -> Cow<'db, TupleSpec<'db>> { + pub fn iterate(self, db: &'db dyn Db) -> Cow<'db, TupleSpec<'db>> { self.try_iterate(db) .unwrap_or_else(|err| Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db)))) } @@ -32,14 +32,14 @@ impl<'db> Type<'db> { /// ```python /// y(*x) /// ``` - pub(super) fn try_iterate( + pub fn try_iterate( self, db: &'db dyn Db, ) -> Result>, IterationError<'db>> { self.try_iterate_with_mode(db, EvaluationMode::Sync) } - pub(super) fn try_iterate_with_mode( + pub fn try_iterate_with_mode( self, db: &'db dyn Db, mode: EvaluationMode, @@ -367,7 +367,7 @@ impl<'db> Type<'db> { /// Error returned if a type is not (or may not be) iterable. #[derive(Debug)] -pub(super) enum IterationError<'db> { +pub enum IterationError<'db> { /// The object being iterated over has a bound `__(a)iter__` method, /// but calling it with the expected arguments results in an error. IterCallError { @@ -412,7 +412,7 @@ pub(super) enum IterationError<'db> { } impl<'db> IterationError<'db> { - pub(super) fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { + pub fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { self.element_type(db).unwrap_or(Type::unknown()) } @@ -503,7 +503,7 @@ impl<'db> IterationError<'db> { } /// Reports the diagnostic for this error. - pub(super) fn report_diagnostic( + pub fn report_diagnostic( &self, context: &InferContext<'db, '_>, iterable_type: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 8bd62585760e9..1a1158c3310b0 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -26,7 +26,7 @@ use crate::{ #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct InternedConstraintSet<'db> { #[returns(ref)] - pub(super) constraints: OwnedConstraintSet<'db>, + pub constraints: OwnedConstraintSet<'db>, } // The Salsa heap is tracked separately. @@ -106,7 +106,7 @@ pub enum KnownInstanceType<'db> { NamedTupleSpec(NamedTupleSpec<'db>), } -pub(super) fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, known_instance: KnownInstanceType<'db>, visitor: &V, @@ -170,7 +170,7 @@ impl<'db> VarianceInferable<'db> for KnownInstanceType<'db> { } impl<'db> KnownInstanceType<'db> { - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -220,7 +220,7 @@ impl<'db> KnownInstanceType<'db> { } } - pub(super) fn class(self, db: &'db dyn Db) -> KnownClass { + pub fn class(self, db: &'db dyn Db) -> KnownClass { match self { Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm, Self::TypeVar(typevar_instance) if typevar_instance.is_paramspec(db) => { @@ -247,7 +247,7 @@ impl<'db> KnownInstanceType<'db> { } } - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { self.class(db).to_class_literal(db) } @@ -256,21 +256,21 @@ impl<'db> KnownInstanceType<'db> { /// For example, an alias created using the `type` statement is an instance of /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db)` /// returns `Type::NominalInstance(NominalInstanceType { class: })`. - pub(super) fn instance_fallback(self, db: &dyn Db) -> Type<'_> { + pub fn instance_fallback(self, db: &dyn Db) -> Type<'_> { self.class(db).to_instance(db) } /// Return `true` if this symbol is an instance of `class`. - pub(super) fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { + pub fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { self.class(db).is_subclass_of(db, class) } /// Return the repr of the symbol at runtime - pub(super) fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { + pub fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { self.display_with(db, DisplaySettings::default()) } - pub(super) fn apply_type_mapping_impl( + pub fn apply_type_mapping_impl( self, db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, @@ -416,13 +416,13 @@ pub struct UnionTypeInstance<'db> { /// `Ok(int | str)`. If any of the element types could not be converted, this /// contains the first encountered error. #[returns(ref)] - pub(super) union_type: Result, InvalidTypeExpressionError<'db>>, + pub union_type: Result, InvalidTypeExpressionError<'db>>, } impl get_size2::GetSize for UnionTypeInstance<'_> {} impl<'db> UnionTypeInstance<'db> { - pub(crate) fn from_value_expression_types( + pub fn from_value_expression_types( db: &'db dyn Db, value_expr_types: [Type<'db>; 2], scope_id: ScopeId<'db>, @@ -448,7 +448,7 @@ impl<'db> UnionTypeInstance<'db> { ))) } - pub(super) fn apply_type_mapping_impl( + pub fn apply_type_mapping_impl( self, db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, @@ -472,7 +472,7 @@ impl<'db> UnionTypeInstance<'db> { /// legacy `typing.Union[…]` annotation, we turn the type-expression types into /// their corresponding value-expression types, i.e. we turn instances like `int` /// into class literals like ``. This operation is potentially lossy. - pub(crate) fn value_expression_types( + pub fn value_expression_types( self, db: &'db dyn Db, ) -> Result> + 'db, InvalidTypeExpressionError<'db>> { @@ -539,7 +539,7 @@ impl<'db> UnionTypeInstance<'db> { /// A salsa-interned `Type` #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct InternedType<'db> { - pub(super) inner: Type<'db>, + pub inner: Type<'db>, } impl get_size2::GetSize for InternedType<'_> {} diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index a930098458646..3292a44262c83 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -29,7 +29,7 @@ use crate::{ /// Iterate over all declarations and bindings that exist at the end /// of the given scope. -pub(crate) fn all_end_of_scope_members<'db>( +pub fn all_end_of_scope_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, ) -> impl Iterator> + 'db { @@ -80,7 +80,7 @@ pub(crate) fn all_end_of_scope_members<'db>( /// Iterate over all declarations and bindings that are reachable anywhere /// in the given scope. -pub(crate) fn all_reachable_members<'db>( +pub fn all_reachable_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, ) -> impl Iterator> + 'db { diff --git a/crates/ty_python_semantic/src/types/literal.rs b/crates/ty_python_semantic/src/types/literal.rs index 9f6839f5e5167..f869974aa4414 100644 --- a/crates/ty_python_semantic/src/types/literal.rs +++ b/crates/ty_python_semantic/src/types/literal.rs @@ -27,7 +27,7 @@ enum LiteralValueTypeInner<'db> { } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) enum LiteralValueTypeKind<'db> { +pub enum LiteralValueTypeKind<'db> { /// An integer literal Int(IntLiteralType), /// A boolean literal, either `True` or `False`. @@ -44,7 +44,7 @@ pub(crate) enum LiteralValueTypeKind<'db> { } impl<'db> LiteralValueType<'db> { - pub(crate) fn new( + pub fn new( kind: impl Into>, is_promotable: bool, ) -> LiteralValueType<'db> { @@ -56,7 +56,7 @@ impl<'db> LiteralValueType<'db> { } /// Creates a literal value that may be promoted. - pub(crate) fn promotable(kind: impl Into>) -> LiteralValueType<'db> { + pub fn promotable(kind: impl Into>) -> LiteralValueType<'db> { let repr = match kind.into() { LiteralValueTypeKind::Int(int) => LiteralValueTypeInner::PromotableInt(int), LiteralValueTypeKind::Bool(bool) => LiteralValueTypeInner::PromotableBool(bool), @@ -70,9 +70,7 @@ impl<'db> LiteralValueType<'db> { } /// Creates a literal value that should not be promoted. - pub(crate) fn unpromotable( - kind: impl Into>, - ) -> LiteralValueType<'db> { + pub fn unpromotable(kind: impl Into>) -> LiteralValueType<'db> { let repr = match kind.into() { LiteralValueTypeKind::Int(int) => LiteralValueTypeInner::UnpromotableInt(int), LiteralValueTypeKind::Bool(bool) => LiteralValueTypeInner::UnpromotableBool(bool), @@ -89,7 +87,7 @@ impl<'db> LiteralValueType<'db> { /// Returns the unpromotable form of this literal value. #[must_use] - pub(crate) fn to_unpromotable(self) -> Self { + pub fn to_unpromotable(self) -> Self { let repr = match self.0 { LiteralValueTypeInner::PromotableInt(int) => { LiteralValueTypeInner::UnpromotableInt(int) @@ -119,7 +117,7 @@ impl<'db> LiteralValueType<'db> { } /// Returns `true` if this literal value should be eagerly promoted to its instance type. - pub(crate) fn is_promotable(self) -> bool { + pub fn is_promotable(self) -> bool { match self.0 { LiteralValueTypeInner::PromotableInt(_) | LiteralValueTypeInner::PromotableBool(_) @@ -137,7 +135,7 @@ impl<'db> LiteralValueType<'db> { } } - pub(crate) fn kind(self) -> LiteralValueTypeKind<'db> { + pub fn kind(self) -> LiteralValueTypeKind<'db> { match self.0 { LiteralValueTypeInner::UnpromotableInt(int) | LiteralValueTypeInner::PromotableInt(int) => LiteralValueTypeKind::Int(int), @@ -156,7 +154,7 @@ impl<'db> LiteralValueType<'db> { } } - pub(crate) fn as_bytes(self) -> Option> { + pub fn as_bytes(self) -> Option> { if let LiteralValueTypeKind::Bytes(v) = self.kind() { Some(v) } else { @@ -164,7 +162,7 @@ impl<'db> LiteralValueType<'db> { } } - pub(crate) fn as_enum(self) -> Option> { + pub fn as_enum(self) -> Option> { if let LiteralValueTypeKind::Enum(v) = self.kind() { Some(v) } else { @@ -172,7 +170,7 @@ impl<'db> LiteralValueType<'db> { } } - pub(crate) fn as_string(self) -> Option> { + pub fn as_string(self) -> Option> { if let LiteralValueTypeKind::String(v) = self.kind() { Some(v) } else { @@ -180,7 +178,7 @@ impl<'db> LiteralValueType<'db> { } } - pub(crate) fn as_bool(self) -> Option { + pub fn as_bool(self) -> Option { if let LiteralValueTypeKind::Bool(v) = self.kind() { Some(v) } else { @@ -188,7 +186,7 @@ impl<'db> LiteralValueType<'db> { } } - pub(crate) fn as_int(self) -> Option { + pub fn as_int(self) -> Option { if let LiteralValueTypeKind::Int(v) = self.kind() { Some(v.as_i64()) } else { @@ -196,19 +194,19 @@ impl<'db> LiteralValueType<'db> { } } - pub(crate) fn is_int(self) -> bool { + pub fn is_int(self) -> bool { matches!(self.kind(), LiteralValueTypeKind::Int(..)) } - pub(crate) fn is_bool(self) -> bool { + pub fn is_bool(self) -> bool { matches!(self.kind(), LiteralValueTypeKind::Bool(..)) } - pub(crate) fn is_literal_string(self) -> bool { + pub fn is_literal_string(self) -> bool { matches!(self.kind(), LiteralValueTypeKind::LiteralString) } - pub(crate) fn is_string(self) -> bool { + pub fn is_string(self) -> bool { matches!(self.kind(), LiteralValueTypeKind::String(..)) } @@ -216,11 +214,11 @@ impl<'db> LiteralValueType<'db> { matches!(self.kind(), LiteralValueTypeKind::Enum(..)) } - pub(crate) fn is_bytes(self) -> bool { + pub fn is_bytes(self) -> bool { matches!(self.kind(), LiteralValueTypeKind::Bytes(..)) } - pub(crate) fn fallback_instance(self, db: &'db dyn Db) -> Type<'db> { + pub fn fallback_instance(self, db: &'db dyn Db) -> Type<'db> { match self.kind() { LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString => { KnownClass::Str.to_instance(db) @@ -272,18 +270,18 @@ impl<'db> From> for Type<'db> { // This type has the same alignment as `salsa::Id`, allowing `LiteralValueType` to use a smaller // discriminant. #[derive(PartialOrd, Ord, Copy, Clone, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) struct IntLiteralType { +pub struct IntLiteralType { high: u32, low: u32, } impl IntLiteralType { - pub(crate) fn as_i64(self) -> i64 { + pub fn as_i64(self) -> i64 { (i64::from(self.high) << 32) | i64::from(self.low) } #[expect(clippy::cast_possible_truncation)] - pub(crate) fn from_i64(value: i64) -> Self { + pub fn from_i64(value: i64) -> Self { let value = value.cast_unsigned(); Self { @@ -308,7 +306,7 @@ impl std::fmt::Debug for IntLiteralType { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct StringLiteralType<'db> { #[returns(deref)] - pub(crate) value: CompactString, + pub value: CompactString, } // The Salsa heap is tracked separately. @@ -316,7 +314,7 @@ impl get_size2::GetSize for StringLiteralType<'_> {} impl<'db> StringLiteralType<'db> { /// The length of the string, as would be returned by Python's `len()`. - pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { + pub fn python_len(self, db: &'db dyn Db) -> usize { self.value(db).chars().count() } } @@ -324,14 +322,14 @@ impl<'db> StringLiteralType<'db> { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct BytesLiteralType<'db> { #[returns(deref)] - pub(crate) value: Box<[u8]>, + pub value: Box<[u8]>, } // The Salsa heap is tracked separately. impl get_size2::GetSize for BytesLiteralType<'_> {} impl<'db> BytesLiteralType<'db> { - pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { + pub fn python_len(self, db: &'db dyn Db) -> usize { self.value(db).len() } } @@ -348,17 +346,17 @@ impl<'db> BytesLiteralType<'db> { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct EnumLiteralType<'db> { /// A reference to the enum class this literal belongs to - pub(crate) enum_class: ClassLiteral<'db>, + pub enum_class: ClassLiteral<'db>, /// The name of the enum member #[returns(ref)] - pub(crate) name: Name, + pub name: Name, } // The Salsa heap is tracked separately. impl get_size2::GetSize for EnumLiteralType<'_> {} impl<'db> EnumLiteralType<'db> { - pub(crate) fn enum_class_instance(self, db: &'db dyn Db) -> Type<'db> { + pub fn enum_class_instance(self, db: &'db dyn Db) -> Type<'db> { self.enum_class(db).to_non_generic_instance(db) } } diff --git a/crates/ty_python_semantic/src/types/member.rs b/crates/ty_python_semantic/src/types/member.rs index d185f2191e90b..17d499de5225a 100644 --- a/crates/ty_python_semantic/src/types/member.rs +++ b/crates/ty_python_semantic/src/types/member.rs @@ -9,42 +9,42 @@ use crate::types::Type; /// The return type of certain member-lookup operations. Contains information /// about the type, type qualifiers, boundness/declaredness. #[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize, Default)] -pub(super) struct Member<'db> { +pub struct Member<'db> { /// Type, qualifiers, and boundness information of this member - pub(super) inner: PlaceAndQualifiers<'db>, + pub inner: PlaceAndQualifiers<'db>, } impl<'db> Member<'db> { - pub(super) fn unbound() -> Self { + pub fn unbound() -> Self { Self { inner: PlaceAndQualifiers::unbound(), } } - pub(super) fn definitely_declared(ty: Type<'db>) -> Self { + pub fn definitely_declared(ty: Type<'db>) -> Self { Self { inner: Place::declared(ty).into(), } } /// Returns the type qualifiers of this member. - pub(super) fn qualifiers(&self) -> crate::types::TypeQualifiers { + pub fn qualifiers(&self) -> crate::types::TypeQualifiers { self.inner.qualifiers } /// Returns `true` if the inner place is undefined (i.e. there is no such member). - pub(super) fn is_undefined(&self) -> bool { + pub fn is_undefined(&self) -> bool { self.inner.place.is_undefined() } /// Returns the inner type, unless it is definitely undefined. - pub(super) fn ignore_possibly_undefined(&self) -> Option> { + pub fn ignore_possibly_undefined(&self) -> Option> { self.inner.place.ignore_possibly_undefined() } /// Map a type transformation function over the type of this member. #[must_use] - pub(super) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { + pub fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { Self { inner: self.inner.map_type(f), } @@ -53,7 +53,7 @@ impl<'db> Member<'db> { /// Infer the public type of a class member/symbol (its type as seen from outside its scope) in the given /// `scope`. -pub(super) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Member<'db> { +pub fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Member<'db> { place_table(db, scope) .symbol_id(name) .map(|symbol_id| { diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index e2d7462194142..1936f5395865b 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -25,16 +25,16 @@ use crate::{ pub struct BoundMethodType<'db> { /// The function that is being bound. Corresponds to the `__func__` attribute on a /// bound method object - pub(crate) function: FunctionType<'db>, + pub function: FunctionType<'db>, /// The instance on which this method has been called. Corresponds to the `__self__` /// attribute on a bound method object - pub(super) self_instance: Type<'db>, + pub self_instance: Type<'db>, } // The Salsa heap is tracked separately. impl get_size2::GetSize for BoundMethodType<'_> {} -pub(super) fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, method: BoundMethodType<'db>, visitor: &V, @@ -56,7 +56,7 @@ impl<'db> BoundMethodType<'db> { /// Returns the type that replaces any `typing.Self` annotations in the bound method signature. /// This is normally the bound-instance type (the type of `self` or `cls`), but if the bound method is /// a `@classmethod`, then it should be an instance of that bound-instance type. - pub(crate) fn typing_self_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn typing_self_type(self, db: &'db dyn Db) -> Type<'db> { let mut self_instance = self.self_instance(db); if self.function(db).is_classmethod(db) { self_instance = self_instance.to_instance(db).unwrap_or_else(Type::unknown); @@ -64,16 +64,12 @@ impl<'db> BoundMethodType<'db> { self_instance } - pub(crate) fn map_self_type( - self, - db: &'db dyn Db, - f: impl FnOnce(Type<'db>) -> Type<'db>, - ) -> Self { + pub fn map_self_type(self, db: &'db dyn Db, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { Self::new(db, self.function(db), f(self.self_instance(db))) } #[salsa::tracked(cycle_initial=into_callable_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { + pub fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { let function = self.function(db); let self_instance = self.typing_self_type(db); @@ -90,7 +86,7 @@ impl<'db> BoundMethodType<'db> { ) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -106,7 +102,7 @@ impl<'db> BoundMethodType<'db> { } #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -174,7 +170,7 @@ pub enum KnownBoundMethodType<'db> { ConstraintSetSatisfiedByAllTypeVars(InternedConstraintSet<'db>), } -pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, method_wrapper: KnownBoundMethodType<'db>, visitor: &V, @@ -209,7 +205,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size impl<'db> KnownBoundMethodType<'db> { #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -317,7 +313,7 @@ impl<'db> KnownBoundMethodType<'db> { } } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -355,7 +351,7 @@ impl<'db> KnownBoundMethodType<'db> { } /// Return the [`KnownClass`] that inhabitants of this type are instances of at runtime - pub(super) fn class(self) -> KnownClass { + pub fn class(self) -> KnownClass { match self { KnownBoundMethodType::FunctionTypeDunderGet(_) | KnownBoundMethodType::FunctionTypeDunderCall(_) @@ -376,7 +372,7 @@ impl<'db> KnownBoundMethodType<'db> { /// Return the signatures of this bound method type. /// /// If the bound method type is overloaded, it may have multiple signatures. - pub(super) fn signatures(self, db: &'db dyn Db) -> impl Iterator> { + pub fn signatures(self, db: &'db dyn Db) -> impl Iterator> { match self { // Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`. // This is required because we need to return more precise types than what the signature in @@ -568,7 +564,7 @@ pub enum WrapperDescriptorKind { } impl WrapperDescriptorKind { - pub(super) fn signatures(self, db: &dyn Db) -> impl Iterator> { + pub fn signatures(self, db: &dyn Db) -> impl Iterator> { /// Similar to what we do in [`KnownBoundMethod::signatures`], /// here we also model `types.FunctionType.__get__` (or builtins.property.__get__), /// but now we consider a call to this as a function, i.e. we also expect the `self` diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 5fa1e766a6959..721ffd8d4e3fc 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -34,7 +34,7 @@ use itertools::Itertools; /// /// See [`ClassType::iter_mro`] for more details. #[derive(PartialEq, Eq, Clone, Debug, salsa::Update, get_size2::GetSize)] -pub(crate) struct Mro<'db>(Box<[ClassBase<'db>]>); +pub struct Mro<'db>(Box<[ClassBase<'db>]>); impl<'db> Mro<'db> { /// Attempt to resolve the MRO of a given class. Because we derive the MRO from the list of @@ -51,7 +51,7 @@ impl<'db> Mro<'db> { /// /// (We emit a diagnostic warning about the runtime `TypeError` in /// [`super::infer::infer_scope_types`].) - pub(super) fn of_static_class( + pub fn of_static_class( db: &'db dyn Db, class_literal: StaticClassLiteral<'db>, specialization: Option>, @@ -323,7 +323,7 @@ impl<'db> Mro<'db> { } } - pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self { + pub fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self { Self::from([ ClassBase::Class(class), ClassBase::unknown(), @@ -334,7 +334,7 @@ impl<'db> Mro<'db> { /// Attempt to resolve the MRO of a dynamic class (created via `type(name, bases, dict)`). /// /// Uses C3 linearization when possible, returning an error if the MRO cannot be resolved. - pub(super) fn of_dynamic_class( + pub fn of_dynamic_class( db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>, ) -> Result> { @@ -422,7 +422,7 @@ impl<'db> Mro<'db> { /// Compute a fallback MRO for a dynamic class when `of_dynamic_class` fails. /// /// Iterates over base MROs sequentially with deduplication. - pub(super) fn dynamic_fallback(db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>) -> Self { + pub fn dynamic_fallback(db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>) -> Self { let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic.into())); let mut result = vec![self_base]; let mut seen = FxHashSet::default(); @@ -488,7 +488,7 @@ impl<'db> FromIterator> for Mro<'db> { /// Even for first-party code, where we will have to resolve the MRO for every class we encounter, /// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the /// Salsa-tracked [`StaticClassLiteral::try_mro`] method unless it's absolutely necessary. -pub(crate) struct MroIterator<'db> { +pub struct MroIterator<'db> { db: &'db dyn Db, /// The class whose MRO we're iterating over @@ -509,7 +509,7 @@ pub(crate) struct MroIterator<'db> { } impl<'db> MroIterator<'db> { - pub(super) fn new( + pub fn new( db: &'db dyn Db, class: ClassLiteral<'db>, specialization: Option>, @@ -598,36 +598,36 @@ impl DoubleEndedIterator for MroIterator<'_> { } #[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(super) struct StaticMroError<'db> { +pub struct StaticMroError<'db> { kind: StaticMroErrorKind<'db>, fallback_mro: Mro<'db>, } impl<'db> StaticMroError<'db> { /// Construct an MRO error of kind `InheritanceCycle`. - pub(super) fn cycle(db: &'db dyn Db, class: ClassType<'db>) -> Self { + pub fn cycle(db: &'db dyn Db, class: ClassType<'db>) -> Self { StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class) } - pub(super) fn is_cycle(&self) -> bool { + pub fn is_cycle(&self) -> bool { matches!(self.kind, StaticMroErrorKind::InheritanceCycle) } /// Return an [`StaticMroErrorKind`] variant describing why we could not resolve the MRO for this class. - pub(super) fn reason(&self) -> &StaticMroErrorKind<'db> { + pub fn reason(&self) -> &StaticMroErrorKind<'db> { &self.kind } /// Return the fallback MRO we should infer for this class during type inference /// (since accurate resolution of its "true" MRO was impossible) - pub(super) fn fallback_mro(&self) -> &Mro<'db> { + pub fn fallback_mro(&self) -> &Mro<'db> { &self.fallback_mro } } /// Possible ways in which attempting to resolve the MRO of a statically-defined class might fail. #[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(super) enum StaticMroErrorKind<'db> { +pub enum StaticMroErrorKind<'db> { /// The class inherits from one or more invalid bases. /// /// To avoid excessive complexity in our implementation, @@ -665,11 +665,7 @@ pub(super) enum StaticMroErrorKind<'db> { } impl<'db> StaticMroErrorKind<'db> { - pub(super) fn into_mro_error( - self, - db: &'db dyn Db, - class: ClassType<'db>, - ) -> StaticMroError<'db> { + pub fn into_mro_error(self, db: &'db dyn Db, class: ClassType<'db>) -> StaticMroError<'db> { StaticMroError { kind: self, fallback_mro: Mro::from_error(db, class), @@ -679,13 +675,13 @@ impl<'db> StaticMroErrorKind<'db> { /// Error recording the fact that a class definition was found to have duplicate bases. #[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(super) struct DuplicateBaseError<'db> { +pub struct DuplicateBaseError<'db> { /// The base that is duplicated in the class's bases list. - pub(super) duplicate_base: ClassBase<'db>, + pub duplicate_base: ClassBase<'db>, /// The index of the first occurrence of the base in the class's bases list. - pub(super) first_index: usize, + pub first_index: usize, /// The indices of the base's later occurrences in the class's bases list. - pub(super) later_indices: Box<[usize]>, + pub later_indices: Box<[usize]>, } /// Implementation of the [C3-merge algorithm] for calculating a Python class's @@ -779,19 +775,19 @@ fn check_generic_reorder_fixes_mro<'db>( /// /// Separate from [`StaticMroError`] because dynamic classes can only have a subset of MRO errors. #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)] -pub(crate) struct DynamicMroError<'db> { +pub struct DynamicMroError<'db> { kind: DynamicMroErrorKind<'db>, fallback_mro: Mro<'db>, } impl<'db> DynamicMroError<'db> { /// Return the error kind describing why we could not resolve the MRO. - pub(crate) fn reason(&self) -> &DynamicMroErrorKind<'db> { + pub fn reason(&self) -> &DynamicMroErrorKind<'db> { &self.kind } /// Return the fallback MRO to use for type inference. - pub(crate) fn fallback_mro(&self) -> &Mro<'db> { + pub fn fallback_mro(&self) -> &Mro<'db> { &self.fallback_mro } } @@ -800,7 +796,7 @@ impl<'db> DynamicMroError<'db> { /// /// These mirror the relevant variants from `MroErrorKind` for static classes. #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)] -pub(crate) enum DynamicMroErrorKind<'db> { +pub enum DynamicMroErrorKind<'db> { /// The class inherits from one or more invalid bases. /// /// Similar to `StaticMroErrorKind::InvalidBases`, this records the indices diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 73215a60d30e8..5cf0ca5a2e771 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -37,7 +37,7 @@ use std::collections::hash_map::Entry; /// This is a conservative upper bound - all places that actually get narrowed /// will be in this set, but there may be additional places that end up not /// being narrowed after full analysis. -pub(crate) type PossiblyNarrowedPlaces = FxHashSet; +pub type PossiblyNarrowedPlaces = FxHashSet; /// Return the type constraint that `test` (if true) would place on `symbol`, if any. /// @@ -55,7 +55,7 @@ pub(crate) type PossiblyNarrowedPlaces = FxHashSet; /// /// But if we called this with the same `test` expression, but the `symbol` of `y`, no /// constraint is applied to that symbol, so we'd just return `None`. -pub(crate) fn infer_narrowing_constraint<'db>( +pub fn infer_narrowing_constraint<'db>( db: &'db dyn Db, predicate: Predicate<'db>, place: ScopedPlaceId, @@ -360,7 +360,7 @@ impl<'db> Conjunctions<'db> { /// => `NarrowingConstraint { intersection_disjuncts: [A], replacement_disjuncts: [B] }` /// => evaluates to `(P & A) | B`, where `P` is our previously-known type #[derive(Hash, PartialEq, Debug, Eq, Clone, salsa::Update, get_size2::GetSize)] -pub(crate) struct NarrowingConstraint<'db> { +pub struct NarrowingConstraint<'db> { /// Intersection constraint (from `isinstance()` narrowing comparisons, `TypeIs`, and /// similar). We keep these as a disjunction of conjunctions to avoid constructing /// union/intersection types while merging constraints. @@ -377,7 +377,7 @@ pub(crate) struct NarrowingConstraint<'db> { impl<'db> NarrowingConstraint<'db> { /// Create an "intersection" constraint: the previous type will be /// intersected with this constraint - pub(crate) fn intersection(constraint: Type<'db>) -> Self { + pub fn intersection(constraint: Type<'db>) -> Self { Self { intersection_disjuncts: smallvec_inline![Conjunctions::singleton(constraint)], replacement_disjuncts: smallvec![], @@ -395,7 +395,7 @@ impl<'db> NarrowingConstraint<'db> { /// Merge two constraints, taking their intersection but respecting "replacement" semantics (with /// `other` winning) - pub(crate) fn merge_constraint_and(&self, other: Self) -> Self { + pub fn merge_constraint_and(&self, other: Self) -> Self { // Distribute AND over OR: (A1 | A2 | ...) AND (B1 | B2 | ...) // becomes (A1 & B1) | (A1 & B2) | ... | (A2 & B1) | ... // @@ -449,7 +449,7 @@ impl<'db> NarrowingConstraint<'db> { /// Evaluate the type this effectively constrains to /// /// Forgets whether each constraint originated from a `replacement` disjunct or not - pub(crate) fn evaluate_constraint_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn evaluate_constraint_type(self, db: &'db dyn Db) -> Type<'db> { let mut union = UnionBuilder::new(db); for conjunctions in self .replacement_disjuncts @@ -2212,23 +2212,23 @@ fn all_matching_tuple_elements_have_literal_types<'db>( /// /// This mirrors the structure of `NarrowingConstraintsBuilder` but only computes which places /// *could* be narrowed, without performing type inference to determine the actual constraints. -pub(crate) struct PossiblyNarrowedPlacesBuilder<'db, 'a> { +pub struct PossiblyNarrowedPlacesBuilder<'db, 'a> { db: &'db dyn Db, places: &'a PlaceTableBuilder, } impl<'db, 'a> PossiblyNarrowedPlacesBuilder<'db, 'a> { - pub(crate) fn new(db: &'db dyn Db, places: &'a PlaceTableBuilder) -> Self { + pub fn new(db: &'db dyn Db, places: &'a PlaceTableBuilder) -> Self { Self { db, places } } /// Compute possibly narrowed places for an expression predicate. - pub(crate) fn expression(self, expr: &ast::Expr) -> PossiblyNarrowedPlaces { + pub fn expression(self, expr: &ast::Expr) -> PossiblyNarrowedPlaces { self.expression_node(expr) } /// Compute possibly narrowed places for a pattern predicate. - pub(crate) fn pattern( + pub fn pattern( self, pattern: PatternPredicate<'db>, module: &ParsedModuleRef, diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index 08cc882ee4afb..33efb042ce276 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -107,7 +107,7 @@ impl<'db> NewType<'db> { Type::object() } - pub(crate) fn is_equivalent_to_impl(self, db: &'db dyn Db, other: Self) -> bool { + pub fn is_equivalent_to_impl(self, db: &'db dyn Db, other: Self) -> bool { // Two instances of the "same" `NewType` won't compare == if one of them has an eagerly // evaluated base (or a normalized base, etc.) and the other doesn't, so we only check for // equality of the `definition`. @@ -117,7 +117,7 @@ impl<'db> NewType<'db> { // Since a regular class can't inherit from a newtype, the only way for one newtype to be a // subtype of another is to have the other in its chain of newtype bases. Once we reach the // base class, we don't have to keep looking. - pub(crate) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -136,7 +136,7 @@ impl<'db> NewType<'db> { ConstraintSet::from_bool(constraints, false) } - pub(crate) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -157,7 +157,7 @@ impl<'db> NewType<'db> { /// Create a new `NewType` by mapping the underlying `ClassType`. This descends through any /// number of nested `NewType` layers and rebuilds the whole chain. In the rare case of cyclic /// `NewType`s with no underlying `ClassType`, this has no effect and does not call `f`. - pub(crate) fn try_map_base_class_type( + pub fn try_map_base_class_type( self, db: &'db dyn Db, f: impl FnOnce(ClassType<'db>) -> Option>, @@ -206,7 +206,7 @@ impl<'db> NewType<'db> { Some(self) } - pub(crate) fn map_base_class_type( + pub fn map_base_class_type( self, db: &'db dyn Db, f: impl FnOnce(ClassType<'db>) -> ClassType<'db>, @@ -216,7 +216,7 @@ impl<'db> NewType<'db> { } } -pub(crate) fn walk_newtype_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_newtype_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, newtype: NewType<'db>, visitor: &V, diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 83f13747e4ad7..63ff10fded44a 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -60,7 +60,7 @@ const PROHIBITED_NAMEDTUPLE_ATTRS: &[&str] = &[ // TODO: Support dynamic class literals. If we allow dynamic classes to define attributes in their // namespace dictionary, we should also check whether those attributes are valid overrides of // attributes in their superclasses. -pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>) { +pub fn check_class<'db>(context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>) { let db = context.db(); let configuration = OverrideRulesConfig::from(context); if configuration.no_rules_enabled() { @@ -513,7 +513,7 @@ fn check_class_declaration<'db>( } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub(super) enum MethodKind<'db> { +pub enum MethodKind<'db> { Synthesized(CodeGeneratorKind<'db>), #[default] NotSynthesized, diff --git a/crates/ty_python_semantic/src/types/property_tests/setup.rs b/crates/ty_python_semantic/src/types/property_tests/setup.rs index b436c4d9e92fc..aa93e55b95413 100644 --- a/crates/ty_python_semantic/src/types/property_tests/setup.rs +++ b/crates/ty_python_semantic/src/types/property_tests/setup.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex, OnceLock}; static CACHED_DB: OnceLock>> = OnceLock::new(); -pub(crate) fn get_cached_db() -> TestDb { +pub fn get_cached_db() -> TestDb { let db = CACHED_DB.get_or_init(|| Arc::new(Mutex::new(setup_db()))); db.lock().unwrap().clone() } diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 671a69ca1970b..154306f8bc948 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -17,7 +17,7 @@ use ty_module_resolver::KnownModule; /// /// TODO: We should add some variants that exercise generic classes and specializations thereof. #[derive(Debug, Clone, PartialEq)] -pub(crate) enum Ty { +pub enum Ty { Never, Unknown, None, @@ -68,13 +68,13 @@ pub(crate) enum Ty { } #[derive(Debug, Clone, PartialEq)] -pub(crate) enum CallableParams { +pub enum CallableParams { GradualForm, List(Vec), } impl CallableParams { - pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> { + pub fn into_parameters(self, db: &TestDb) -> Parameters<'_> { match self { CallableParams::GradualForm => Parameters::gradual_form(), CallableParams::List(params) => Parameters::new( @@ -101,7 +101,7 @@ impl CallableParams { } #[derive(Debug, Clone, PartialEq)] -pub(crate) struct Param { +pub struct Param { kind: ParamKind, name: Option, annotated_ty: Ty, @@ -131,7 +131,7 @@ fn create_bound_method<'db>( } impl Ty { - pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { + pub fn into_type(self, db: &TestDb) -> Type<'_> { match self { Ty::Never => Type::Never, Ty::Unknown => Type::unknown(), @@ -240,10 +240,10 @@ impl Ty { } #[derive(Debug, Clone, PartialEq)] -pub(crate) struct FullyStaticTy(Ty); +pub struct FullyStaticTy(Ty); impl FullyStaticTy { - pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { + pub fn into_type(self, db: &TestDb) -> Type<'_> { self.0.into_type(db) } } @@ -559,13 +559,10 @@ impl Arbitrary for FullyStaticTy { } } -pub(crate) fn intersection<'db>( - db: &'db TestDb, - tys: impl IntoIterator>, -) -> Type<'db> { +pub fn intersection<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { IntersectionType::from_elements(db, tys) } -pub(crate) fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { +pub fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { UnionType::from_elements(db, tys) } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 87d3ea7066821..2d4290b0f0fe2 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -35,7 +35,7 @@ use crate::{ impl<'db> StaticClassLiteral<'db> { /// Returns `Some` if this is a protocol class, `None` otherwise. - pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option> { + pub fn into_protocol_class(self, db: &'db dyn Db) -> Option> { self.is_protocol(db) .then_some(ProtocolClass(ClassType::NonGeneric(self.into()))) } @@ -43,14 +43,14 @@ impl<'db> StaticClassLiteral<'db> { impl<'db> ClassType<'db> { /// Returns `Some` if this is a protocol class, `None` otherwise. - pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option> { + pub fn into_protocol_class(self, db: &'db dyn Db) -> Option> { self.is_protocol(db).then_some(ProtocolClass(self)) } } /// Representation of a single `Protocol` class definition. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(super) struct ProtocolClass<'db>(ClassType<'db>); +pub struct ProtocolClass<'db>(ClassType<'db>); impl<'db> ProtocolClass<'db> { /// Returns the protocol members of this class. @@ -67,12 +67,12 @@ impl<'db> ProtocolClass<'db> { /// It is illegal for a protocol class to have any instance attributes that are not declared /// in the protocol's class body. If any are assigned to, they are not taken into account in /// the protocol's list of members. - pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + pub fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { let _span = tracing::trace_span!("protocol_members", "class='{}'", self.name(db)).entered(); cached_protocol_interface(db, *self) } - pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool { + pub fn is_runtime_checkable(self, db: &'db dyn Db) -> bool { self.static_class_literal(db) .is_some_and(|(class_literal, _)| { class_literal @@ -84,7 +84,7 @@ impl<'db> ProtocolClass<'db> { /// Iterate through the body of the protocol class. Check that all definitions /// in the protocol class body are either explicitly declared directly in the /// class body, or are declared in a superclass of the protocol class. - pub(super) fn validate_members(self, context: &InferContext) { + pub fn validate_members(self, context: &InferContext) { let db = context.db(); let interface = self.interface(db); let Some((class_literal, _)) = self.static_class_literal(db) else { @@ -141,7 +141,7 @@ impl<'db> ProtocolClass<'db> { } } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -154,7 +154,7 @@ impl<'db> ProtocolClass<'db> { ) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -182,14 +182,14 @@ impl<'db> From> for Type<'db> { /// The interface of a protocol: the members of that protocol, and the types of those members. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub(super) struct ProtocolInterface<'db> { +pub struct ProtocolInterface<'db> { #[returns(ref)] inner: BTreeMap>, } impl get_size2::GetSize for ProtocolInterface<'_> {} -pub(super) fn walk_protocol_interface<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_protocol_interface<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, interface: ProtocolInterface<'db>, visitor: &V, @@ -204,7 +204,7 @@ impl<'db> ProtocolInterface<'db> { /// /// All created members will be covariant, read-only property members /// rather than method members or mutable attribute members. - pub(super) fn with_property_members<'a, M>(db: &'db dyn Db, members: M) -> Self + pub fn with_property_members<'a, M>(db: &'db dyn Db, members: M) -> Self where M: IntoIterator)>, { @@ -239,7 +239,7 @@ impl<'db> ProtocolInterface<'db> { Self::new(db, BTreeMap::default()) } - pub(super) fn members<'a>( + pub fn members<'a>( self, db: &'db dyn Db, ) -> impl ExactSizeIterator> @@ -254,7 +254,7 @@ impl<'db> ProtocolInterface<'db> { }) } - pub(super) fn non_method_members(self, db: &'db dyn Db) -> Vec> { + pub fn non_method_members(self, db: &'db dyn Db) -> Vec> { self.members(db) .filter(|member| !member.is_method() && !member.ty().is_todo()) .collect() @@ -269,12 +269,12 @@ impl<'db> ProtocolInterface<'db> { }) } - pub(super) fn includes_member(self, db: &'db dyn Db, name: &str) -> bool { + pub fn includes_member(self, db: &'db dyn Db, name: &str) -> bool { self.inner(db).contains_key(name) } /// Returns the `__call__` method's callable type if this protocol has a `__call__` method member. - pub(super) fn call_method(self, db: &'db dyn Db) -> Option> { + pub fn call_method(self, db: &'db dyn Db) -> Option> { self.member_by_name(db, "__call__") .and_then(|member| match member.kind { ProtocolMemberKind::Method(callable) => Some(callable), @@ -282,7 +282,7 @@ impl<'db> ProtocolInterface<'db> { }) } - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { self.member_by_name(db, name) .map(|member| PlaceAndQualifiers { place: Place::bound(member.ty()), @@ -292,7 +292,7 @@ impl<'db> ProtocolInterface<'db> { } #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -404,7 +404,7 @@ impl<'db> ProtocolInterface<'db> { }) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -424,7 +424,7 @@ impl<'db> ProtocolInterface<'db> { )) } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -445,7 +445,7 @@ impl<'db> ProtocolInterface<'db> { ) } - pub(super) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -457,7 +457,7 @@ impl<'db> ProtocolInterface<'db> { } } - pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { + pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { struct ProtocolInterfaceDisplay<'db> { db: &'db dyn Db, interface: ProtocolInterface<'db>, @@ -493,7 +493,7 @@ impl<'db> VarianceInferable<'db> for ProtocolInterface<'db> { } #[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update, get_size2::GetSize)] -pub(super) struct ProtocolMemberData<'db> { +pub struct ProtocolMemberData<'db> { kind: ProtocolMemberKind<'db>, qualifiers: TypeQualifiers, definition: Option>, @@ -651,7 +651,7 @@ impl<'db> ProtocolMemberKind<'db> { /// A single member of a protocol interface. #[derive(Debug, PartialEq, Eq)] -pub(super) struct ProtocolMember<'a, 'db> { +pub struct ProtocolMember<'a, 'db> { name: &'a str, kind: ProtocolMemberKind<'db>, qualifiers: TypeQualifiers, @@ -673,19 +673,19 @@ fn walk_protocol_member<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( } impl<'a, 'db> ProtocolMember<'a, 'db> { - pub(super) fn name(&self) -> &'a str { + pub fn name(&self) -> &'a str { self.name } - pub(super) fn qualifiers(&self) -> TypeQualifiers { + pub fn qualifiers(&self) -> TypeQualifiers { self.qualifiers } - pub(super) const fn is_method(&self) -> bool { + pub const fn is_method(&self) -> bool { matches!(self.kind, ProtocolMemberKind::Method(_)) } - pub(super) fn definition(&self) -> Option> { + pub fn definition(&self) -> Option> { self.definition } @@ -697,7 +697,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { } } - pub(super) fn has_disjoint_type_from<'c>( + pub fn has_disjoint_type_from<'c>( &self, db: &'db dyn Db, other: Type<'db>, @@ -725,7 +725,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { /// Return `true` if `other` contains an attribute/method/property that satisfies /// the part of the interface defined by this protocol member. #[expect(clippy::too_many_arguments)] - pub(super) fn is_satisfied_by<'c>( + pub fn is_satisfied_by<'c>( &self, db: &'db dyn Db, other: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 27225620f98a0..b90c9a67206c9 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -21,7 +21,7 @@ use crate::{ /// A non-exhaustive enumeration of relations that can exist between types. #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub(crate) enum TypeRelation { +pub enum TypeRelation { /// The "subtyping" relation. /// /// A [fully static] type `B` is a subtype of a fully static type `A` if and only if @@ -201,19 +201,19 @@ pub(crate) enum TypeRelation { } impl TypeRelation { - pub(crate) const fn is_assignability(self) -> bool { + pub const fn is_assignability(self) -> bool { matches!(self, TypeRelation::Assignability) } - pub(crate) const fn is_constraint_set_assignability(self) -> bool { + pub const fn is_constraint_set_assignability(self) -> bool { matches!(self, TypeRelation::ConstraintSetAssignability) } - pub(crate) const fn is_subtyping(self) -> bool { + pub const fn is_subtyping(self) -> bool { matches!(self, TypeRelation::Subtyping) } - pub(crate) const fn can_safely_assume_reflexivity(self, ty: Type) -> bool { + pub const fn can_safely_assume_reflexivity(self, ty: Type) -> bool { match self { TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability @@ -277,13 +277,13 @@ impl<'db> Type<'db> { /// Return true if this type is a subtype of type `target`. /// /// See [`TypeRelation::Subtyping`] for more details. - pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { let constraints = ConstraintSetBuilder::new(); self.when_subtype_of(db, target, &constraints, InferableTypeVars::None) .is_always_satisfied(db) } - pub(super) fn when_subtype_of<'c>( + pub fn when_subtype_of<'c>( self, db: &'db dyn Db, target: Type<'db>, @@ -297,7 +297,7 @@ impl<'db> Type<'db> { /// all of the restrictions in `constraints` hold. /// /// See [`TypeRelation::SubtypingAssuming`] for more details. - pub(super) fn when_subtype_of_assuming<'c>( + pub fn when_subtype_of_assuming<'c>( self, db: &'db dyn Db, target: Type<'db>, @@ -336,7 +336,7 @@ impl<'db> Type<'db> { .is_always_satisfied(db) } - pub(super) fn when_assignable_to<'c>( + pub fn when_assignable_to<'c>( self, db: &'db dyn Db, target: Type<'db>, @@ -352,7 +352,7 @@ impl<'db> Type<'db> { ) } - pub(super) fn when_constraint_set_assignable_to<'c>( + pub fn when_constraint_set_assignable_to<'c>( self, db: &'db dyn Db, target: Type<'db>, @@ -371,7 +371,7 @@ impl<'db> Type<'db> { /// Return `true` if it would be redundant to add `self` to a union that already contains `other`. /// /// See [`TypeRelation::Redundancy`] for more details. - pub(super) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool { + pub fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool { #[salsa::tracked(cycle_initial=|_, _, _, _| true, heap_size=ruff_memory_usage::heap_size)] fn is_redundant_with_impl<'db>( db: &'db dyn Db, @@ -397,7 +397,7 @@ impl<'db> Type<'db> { is_redundant_with_impl(db, self, other) } - pub(super) fn has_relation_to<'c>( + pub fn has_relation_to<'c>( self, db: &'db dyn Db, target: Type<'db>, @@ -417,7 +417,7 @@ impl<'db> Type<'db> { } #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, target: Type<'db>, @@ -1802,13 +1802,13 @@ impl<'db> Type<'db> { /// > — [Summary of type relations] /// /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { + pub fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { let constraints = ConstraintSetBuilder::new(); self.when_equivalent_to(db, other, &constraints) .is_always_satisfied(db) } - pub(crate) fn when_equivalent_to<'c>( + pub fn when_equivalent_to<'c>( self, db: &'db dyn Db, other: Type<'db>, @@ -1825,7 +1825,7 @@ impl<'db> Type<'db> { ) } - pub(crate) fn when_equivalent_to_impl<'c>( + pub fn when_equivalent_to_impl<'c>( self, db: &'db dyn Db, other: Type<'db>, @@ -1870,13 +1870,13 @@ impl<'db> Type<'db> { /// /// This function aims to have no false positives, but might return wrong /// `false` answers in some cases. - pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { + pub fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { let constraints = ConstraintSetBuilder::new(); self.when_disjoint_from(db, other, &constraints, InferableTypeVars::None) .is_always_satisfied(db) } - pub(crate) fn when_disjoint_from<'c>( + pub fn when_disjoint_from<'c>( self, db: &'db dyn Db, other: Type<'db>, @@ -1893,7 +1893,7 @@ impl<'db> Type<'db> { ) } - pub(crate) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Type<'db>, @@ -2770,7 +2770,7 @@ impl<'db> Type<'db> { } /// A [`PairVisitor`] that is used in `has_relation_to` methods. -pub(crate) type HasRelationToVisitor<'db, 'c> = CycleDetector< +pub type HasRelationToVisitor<'db, 'c> = CycleDetector< TypeRelation, (Type<'db>, Type<'db>, TypeRelation), ConstraintSet<'db, 'c>, @@ -2778,11 +2778,11 @@ pub(crate) type HasRelationToVisitor<'db, 'c> = CycleDetector< >; impl<'db, 'c> HasRelationToVisitor<'db, 'c> { - pub(crate) fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { + pub fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { HasRelationToVisitor::with_given(constraints, ConstraintSet::from_bool(constraints, false)) } - pub(crate) fn with_given( + pub fn with_given( constraints: &'c ConstraintSetBuilder<'db>, given: ConstraintSet<'db, 'c>, ) -> Self { @@ -2792,13 +2792,13 @@ impl<'db, 'c> HasRelationToVisitor<'db, 'c> { } /// A [`PairVisitor`] that is used in `is_disjoint_from` methods. -pub(crate) type IsDisjointVisitor<'db, 'c> = PairVisitor<'db, IsDisjoint, ConstraintSet<'db, 'c>>; +pub type IsDisjointVisitor<'db, 'c> = PairVisitor<'db, IsDisjoint, ConstraintSet<'db, 'c>>; #[derive(Debug)] -pub(crate) struct IsDisjoint; +pub struct IsDisjoint; impl<'db, 'c> IsDisjointVisitor<'db, 'c> { - pub(crate) fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { + pub fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { IsDisjointVisitor::new(ConstraintSet::from_bool(constraints, false)) } } diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs index 5cec8af50eae0..ce98313eb7821 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -6,9 +6,9 @@ use crate::types::visitor; use crate::types::{Type, TypeQualifiers}; use crate::{Db, FxOrderSet}; -pub(crate) mod builder; +pub mod builder; -pub(crate) use builder::{IntersectionBuilder, UnionBuilder}; +pub use builder::{IntersectionBuilder, UnionBuilder}; #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct UnionType<'db> { @@ -17,10 +17,10 @@ pub struct UnionType<'db> { pub elements: Box<[Type<'db>]>, /// Whether the value pointed to by this type is recursively defined. /// If `Yes`, union literal widening is performed early. - pub(crate) recursively_defined: RecursivelyDefined, + pub recursively_defined: RecursivelyDefined, } -pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, union: UnionType<'db>, visitor: &V, @@ -66,7 +66,7 @@ impl<'db> UnionType<'db> { } /// Create a union from a list of elements without unpacking type aliases. - pub(crate) fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> + pub fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> where I: IntoIterator, T: Into>, @@ -80,7 +80,7 @@ impl<'db> UnionType<'db> { .build() } - pub(crate) fn from_elements_cycle_recovery(db: &'db dyn Db, elements: I) -> Type<'db> + pub fn from_elements_cycle_recovery(db: &'db dyn Db, elements: I) -> Type<'db> where I: IntoIterator, T: Into>, @@ -99,7 +99,7 @@ impl<'db> UnionType<'db> { /// If all items in `elements` are `Some()`, the result of unioning all elements is returned. /// As soon as a `None` element in the iterable is encountered, /// the function short-circuits and returns `None`. - pub(crate) fn try_from_elements(db: &'db dyn Db, elements: I) -> Option> + pub fn try_from_elements(db: &'db dyn Db, elements: I) -> Option> where I: IntoIterator>, T: Into>, @@ -113,7 +113,7 @@ impl<'db> UnionType<'db> { /// Apply a transformation function to all elements of the union, /// and create a new union from the resulting set of types. - pub(crate) fn map( + pub fn map( self, db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, @@ -129,7 +129,7 @@ impl<'db> UnionType<'db> { } /// A version of [`UnionType::map`] that does not unpack type aliases. - pub(crate) fn map_leave_aliases( + pub fn map_leave_aliases( self, db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, @@ -152,7 +152,7 @@ impl<'db> UnionType<'db> { /// the result of unioning all transformed elements is returned. /// As soon as `transform_fn` returns `None` for an element, however, /// the function short-circuits and returns `None`. - pub(crate) fn try_map( + pub fn try_map( self, db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Option>, @@ -165,11 +165,11 @@ impl<'db> UnionType<'db> { Some(builder.build()) } - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> { + pub fn to_instance(self, db: &'db dyn Db) -> Option> { self.try_map(db, |element| element.to_instance(db)) } - pub(crate) fn filter(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { + pub fn filter(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { let current = self.elements(db); let new: Box<[Type<'db>]> = current.iter().copied().filter(f).collect(); match &*new { @@ -180,7 +180,7 @@ impl<'db> UnionType<'db> { } } - pub(crate) fn map_with_boundness( + pub fn map_with_boundness( self, db: &'db dyn Db, mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, @@ -231,7 +231,7 @@ impl<'db> UnionType<'db> { } } - pub(crate) fn map_with_boundness_and_qualifiers( + pub fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, @@ -289,7 +289,7 @@ impl<'db> UnionType<'db> { } } - pub(crate) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -331,7 +331,7 @@ impl<'db> UnionType<'db> { /// Identify some specific unions of known classes, currently the ones that `float` and /// `complex` expand into in type position. - pub(crate) fn known(self, db: &'db dyn Db) -> Option { + pub fn known(self, db: &'db dyn Db) -> Option { let mut has_int = false; let mut has_float = false; let mut has_complex = false; @@ -352,13 +352,13 @@ impl<'db> UnionType<'db> { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum KnownUnion { +pub enum KnownUnion { Float, // `int | float` Complex, // `int | float | complex` } impl KnownUnion { - pub(crate) fn to_type(self, db: &dyn Db) -> Type<'_> { + pub fn to_type(self, db: &dyn Db) -> Type<'_> { match self { KnownUnion::Float => UnionType::from_two_elements( db, @@ -381,7 +381,7 @@ impl KnownUnion { pub struct IntersectionType<'db> { /// The intersection type includes only values in all of these types. #[returns(ref)] - pub(crate) positive: FxOrderSet>, + pub positive: FxOrderSet>, /// The intersection type does not include any value in any of these types. /// @@ -389,7 +389,7 @@ pub struct IntersectionType<'db> { /// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them /// directly in intersections rather than as a separate type. #[returns(ref)] - pub(crate) negative: NegativeIntersectionElements<'db>, + pub negative: NegativeIntersectionElements<'db>, } /// To avoid unnecessary allocations for the common case of 1 negative elements, @@ -416,7 +416,7 @@ pub enum NegativeIntersectionElements<'db> { } impl<'db> NegativeIntersectionElements<'db> { - pub(crate) fn iter(&self) -> NegativeIntersectionElementsIterator<'_, 'db> { + pub fn iter(&self) -> NegativeIntersectionElementsIterator<'_, 'db> { match self { Self::Empty => NegativeIntersectionElementsIterator::EmptyOrOne(None), Self::Single(ty) => NegativeIntersectionElementsIterator::EmptyOrOne(Some(ty)), @@ -424,7 +424,7 @@ impl<'db> NegativeIntersectionElements<'db> { } } - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { match self { Self::Empty => 0, Self::Single(_) => 1, @@ -432,7 +432,7 @@ impl<'db> NegativeIntersectionElements<'db> { } } - pub(crate) fn contains(&self, ty: &Type<'db>) -> bool { + pub fn contains(&self, ty: &Type<'db>) -> bool { match self { Self::Empty => false, Self::Single(existing) => existing == ty, @@ -440,7 +440,7 @@ impl<'db> NegativeIntersectionElements<'db> { } } - pub(crate) fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { // See struct-level comment: we don't try to maintain the invariant that empty // collections are representend as `Self::Empty` self.len() == 0 @@ -450,7 +450,7 @@ impl<'db> NegativeIntersectionElements<'db> { /// /// Returns `true` if the elements was newly added. /// Returns `false` if the element was already present in the collection. - pub(crate) fn insert(&mut self, ty: Type<'db>) -> bool { + pub fn insert(&mut self, ty: Type<'db>) -> bool { match self { Self::Empty => { *self = Self::Single(ty); @@ -469,7 +469,7 @@ impl<'db> NegativeIntersectionElements<'db> { } /// Shrink the capacity of the collection as much as possible. - pub(crate) fn shrink_to_fit(&mut self) { + pub fn shrink_to_fit(&mut self) { match self { Self::Empty | Self::Single(_) => {} Self::Multiple(set) => set.shrink_to_fit(), @@ -485,7 +485,7 @@ impl<'db> NegativeIntersectionElements<'db> { /// the last element in the collection is popped off the end of the collection /// and placed at the index where `ty` was previously, allowing this method to complete /// in O(1) time (average). - pub(crate) fn swap_remove(&mut self, ty: &Type<'db>) -> bool { + pub fn swap_remove(&mut self, ty: &Type<'db>) -> bool { match self { Self::Empty => false, Self::Single(existing) => { @@ -507,7 +507,7 @@ impl<'db> NegativeIntersectionElements<'db> { /// The element is removed by swapping it with the last element /// of the collection and popping it off, allowing this method to complete /// in O(1) time (average). - pub(crate) fn swap_remove_index(&mut self, index: usize) -> Option> { + pub fn swap_remove_index(&mut self, index: usize) -> Option> { match self { Self::Empty => None, Self::Single(existing) => { @@ -608,7 +608,7 @@ impl std::iter::FusedIterator for NegativeIntersectionElementsIterator<'_, '_> { // The Salsa heap is tracked separately. impl get_size2::GetSize for IntersectionType<'_> {} -pub(crate) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, intersection: IntersectionType<'db>, visitor: &V, @@ -627,7 +627,7 @@ impl<'db> IntersectionType<'db> { /// /// For performance reasons, consider using [`IntersectionType::from_two_elements`] if /// the intersection is constructed from exactly two elements. - pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> where I: IntoIterator, T: Into>, @@ -645,13 +645,13 @@ impl<'db> IntersectionType<'db> { }, heap_size=ruff_memory_usage::heap_size )] - pub(crate) fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + pub fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { IntersectionBuilder::new(db) .positive_elements([a, b]) .build() } - pub(crate) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -687,10 +687,7 @@ impl<'db> IntersectionType<'db> { /// Returns an iterator over the positive elements of the intersection. If /// there are no positive elements, returns a single `object` type. - pub(crate) fn positive_elements_or_object( - self, - db: &'db dyn Db, - ) -> impl Iterator> { + pub fn positive_elements_or_object(self, db: &'db dyn Db) -> impl Iterator> { if self.positive(db).is_empty() { Either::Left(std::iter::once(Type::object())) } else { @@ -700,7 +697,7 @@ impl<'db> IntersectionType<'db> { /// Map a type transformation over all positive elements of the intersection. Leave the /// negative elements unchanged. - pub(crate) fn map_positive( + pub fn map_positive( self, db: &'db dyn Db, mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, @@ -715,7 +712,7 @@ impl<'db> IntersectionType<'db> { builder.build() } - pub(crate) fn map_with_boundness( + pub fn map_with_boundness( self, db: &'db dyn Db, mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, @@ -762,7 +759,7 @@ impl<'db> IntersectionType<'db> { } } - pub(crate) fn map_with_boundness_and_qualifiers( + pub fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, @@ -825,11 +822,11 @@ impl<'db> IntersectionType<'db> { self.negative(db).iter().copied() } - pub(crate) fn has_one_element(self, db: &'db dyn Db) -> bool { + pub fn has_one_element(self, db: &'db dyn Db) -> bool { (self.positive(db).len() + self.negative(db).len()) == 1 } - pub(crate) fn is_simple_negation(self, db: &'db dyn Db) -> bool { + pub fn is_simple_negation(self, db: &'db dyn Db) -> bool { self.positive(db).is_empty() && self.negative(db).len() == 1 } } diff --git a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs index 291667e3e1145..948585be0a12e 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -328,7 +328,7 @@ const MAX_NON_RECURSIVE_UNION_LITERALS: usize = 256; /// if reachability analysis etc. fails when analysing these enums. const MAX_NON_RECURSIVE_UNION_ENUM_LITERALS: usize = 8192; -pub(crate) struct UnionBuilder<'db> { +pub struct UnionBuilder<'db> { elements: Vec>, db: &'db dyn Db, unpack_aliases: bool, @@ -340,7 +340,7 @@ pub(crate) struct UnionBuilder<'db> { } impl<'db> UnionBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db) -> Self { + pub fn new(db: &'db dyn Db) -> Self { Self { db, elements: vec![], @@ -350,12 +350,12 @@ impl<'db> UnionBuilder<'db> { } } - pub(crate) fn unpack_aliases(mut self, val: bool) -> Self { + pub fn unpack_aliases(mut self, val: bool) -> Self { self.unpack_aliases = val; self } - pub(crate) fn cycle_recovery(mut self, val: bool) -> Self { + pub fn cycle_recovery(mut self, val: bool) -> Self { self.cycle_recovery = val; if self.cycle_recovery { self.unpack_aliases = false; @@ -363,12 +363,12 @@ impl<'db> UnionBuilder<'db> { self } - pub(crate) fn recursively_defined(mut self, val: RecursivelyDefined) -> Self { + pub fn recursively_defined(mut self, val: RecursivelyDefined) -> Self { self.recursively_defined = val; self } - pub(crate) fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.elements.is_empty() } @@ -404,17 +404,17 @@ impl<'db> UnionBuilder<'db> { } /// Adds a type to this union. - pub(crate) fn add(mut self, ty: Type<'db>) -> Self { + pub fn add(mut self, ty: Type<'db>) -> Self { self.add_in_place(ty); self } /// Adds a type to this union. - pub(crate) fn add_in_place(&mut self, ty: Type<'db>) { + pub fn add_in_place(&mut self, ty: Type<'db>) { self.add_in_place_impl(ty, &mut vec![]); } - pub(crate) fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + pub fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { let cycle_recovery = self.cycle_recovery; let should_widen = |literals, recursively_defined: RecursivelyDefined| { if recursively_defined.is_yes() && cycle_recovery { @@ -815,11 +815,11 @@ impl<'db> UnionBuilder<'db> { } } - pub(crate) fn build(self) -> Type<'db> { + pub fn build(self) -> Type<'db> { self.try_build().unwrap_or(Type::Never) } - pub(crate) fn try_build(self) -> Option> { + pub fn try_build(self) -> Option> { let mut types = vec![]; for element in self.elements { match element { @@ -859,7 +859,7 @@ impl<'db> UnionBuilder<'db> { } #[derive(Clone)] -pub(crate) struct IntersectionBuilder<'db> { +pub struct IntersectionBuilder<'db> { // Really this builds a union-of-intersections, because we always keep our set-theoretic types // in disjunctive normal form (DNF), a union of intersections. In the simplest case there's // just a single intersection in this vector, and we are building a single intersection type, @@ -870,7 +870,7 @@ pub(crate) struct IntersectionBuilder<'db> { } impl<'db> IntersectionBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db) -> Self { + pub fn new(db: &'db dyn Db) -> Self { Self { db, intersections: vec![InnerIntersectionBuilder::default()], @@ -884,15 +884,11 @@ impl<'db> IntersectionBuilder<'db> { } } - pub(crate) fn add_positive(self, ty: Type<'db>) -> Self { + pub fn add_positive(self, ty: Type<'db>) -> Self { self.add_positive_impl(ty, &mut vec![]) } - pub(crate) fn add_positive_impl( - mut self, - ty: Type<'db>, - seen_aliases: &mut Vec>, - ) -> Self { + pub fn add_positive_impl(mut self, ty: Type<'db>, seen_aliases: &mut Vec>) -> Self { match ty { Type::TypeAlias(alias) => { if seen_aliases.contains(&ty) { @@ -986,15 +982,11 @@ impl<'db> IntersectionBuilder<'db> { } } - pub(crate) fn add_negative(self, ty: Type<'db>) -> Self { + pub fn add_negative(self, ty: Type<'db>) -> Self { self.add_negative_impl(ty, &mut vec![]) } - pub(crate) fn add_negative_impl( - mut self, - ty: Type<'db>, - seen_aliases: &mut Vec>, - ) -> Self { + pub fn add_negative_impl(mut self, ty: Type<'db>, seen_aliases: &mut Vec>) -> Self { // See comments above in `add_positive`; this is just the negated version. match ty { Type::TypeAlias(alias) => { @@ -1120,7 +1112,7 @@ impl<'db> IntersectionBuilder<'db> { } } - pub(crate) fn positive_elements(mut self, elements: I) -> Self + pub fn positive_elements(mut self, elements: I) -> Self where I: IntoIterator, T: Into>, @@ -1131,7 +1123,7 @@ impl<'db> IntersectionBuilder<'db> { self } - pub(crate) fn build(mut self) -> Type<'db> { + pub fn build(mut self) -> Type<'db> { // Avoid allocating the UnionBuilder unnecessarily if we have just one intersection: if self.intersections.len() == 1 { self.intersections.pop().unwrap().build(self.db) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 3e0f0f2d1dc4f..ac1d1905b6576 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -66,23 +66,23 @@ fn function_signature_expression_type<'db>( pub struct CallableSignature<'db> { /// The signatures of each overload of this callable. Will be empty if the type is not /// callable. - pub(crate) overloads: SmallVec<[Signature<'db>; 1]>, + pub overloads: SmallVec<[Signature<'db>; 1]>, } impl<'db> CallableSignature<'db> { - pub(crate) fn single(signature: Signature<'db>) -> Self { + pub fn single(signature: Signature<'db>) -> Self { Self { overloads: smallvec_inline![signature], } } - pub(crate) fn bottom() -> Self { + pub fn bottom() -> Self { Self::single(Signature::bottom()) } /// Creates a new `CallableSignature` from an iterator of [`Signature`]s. Returns a /// non-callable signature if the iterator is empty. - pub(crate) fn from_overloads(overloads: I) -> Self + pub fn from_overloads(overloads: I) -> Self where I: IntoIterator>, { @@ -91,11 +91,11 @@ impl<'db> CallableSignature<'db> { } } - pub(crate) fn iter(&self) -> std::slice::Iter<'_, Signature<'db>> { + pub fn iter(&self) -> std::slice::Iter<'_, Signature<'db>> { self.overloads.iter() } - pub(crate) fn with_inherited_generic_context( + pub fn with_inherited_generic_context( &self, db: &'db dyn Db, inherited_generic_context: GenericContext<'db>, @@ -107,7 +107,7 @@ impl<'db> CallableSignature<'db> { })) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( &self, db: &'db dyn Db, div: Type<'db>, @@ -122,7 +122,7 @@ impl<'db> CallableSignature<'db> { }) } - pub(crate) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -254,7 +254,7 @@ impl<'db> CallableSignature<'db> { } } - pub(crate) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( &self, db: &'db dyn Db, binding_context: Option>, @@ -269,7 +269,7 @@ impl<'db> CallableSignature<'db> { /// Binds the first (presumably `self`) parameter of this signature. If a `self_type` is /// provided, we will replace any occurrences of `typing.Self` in the parameter and return /// annotations with that type. - pub(crate) fn bind_self(&self, db: &'db dyn Db, self_type: Option>) -> Self { + pub fn bind_self(&self, db: &'db dyn Db, self_type: Option>) -> Self { Self { overloads: self .overloads @@ -282,7 +282,7 @@ impl<'db> CallableSignature<'db> { /// Replaces any occurrences of `typing.Self` in the parameter and return annotations with the /// given type. (Does not bind the `self` parameter; to do that, use /// [`bind_self`][Self::bind_self].) - pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self { + pub fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self { Self { overloads: self .overloads @@ -293,7 +293,7 @@ impl<'db> CallableSignature<'db> { } #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( &self, db: &'db dyn Db, other: &Self, @@ -315,7 +315,7 @@ impl<'db> CallableSignature<'db> { ) } - pub(crate) fn is_single_paramspec(&self) -> Option<(BoundTypeVarInstance<'db>, Type<'db>)> { + pub fn is_single_paramspec(&self) -> Option<(BoundTypeVarInstance<'db>, Type<'db>)> { Self::signatures_is_single_paramspec(&self.overloads) } @@ -335,7 +335,7 @@ impl<'db> CallableSignature<'db> { .map(|bound_typevar| (bound_typevar, signature.return_ty)) } - pub(crate) fn when_constraint_set_assignable_to<'c>( + pub fn when_constraint_set_assignable_to<'c>( &self, db: &'db dyn Db, other: &Self, @@ -679,11 +679,11 @@ impl<'db> VarianceInferable<'db> for &CallableSignature<'db> { #[derive(Clone, Debug, salsa::Update, get_size2::GetSize, PartialEq, Eq, Hash)] pub struct Signature<'db> { /// The generic context for this overload, if it is generic. - pub(crate) generic_context: Option>, + pub generic_context: Option>, /// The original definition associated with this function, if available. /// This is useful for locating and extracting docstring information for the signature. - pub(crate) definition: Option>, + pub definition: Option>, /// Parameters, in source order. /// @@ -696,10 +696,10 @@ pub struct Signature<'db> { parameters: Parameters<'db>, /// Return type. If no annotation was provided, this is `Unknown`. - pub(crate) return_ty: Type<'db>, + pub return_ty: Type<'db>, } -pub(super) fn walk_signature<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_signature<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, signature: &Signature<'db>, visitor: &V, @@ -716,7 +716,7 @@ pub(super) fn walk_signature<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( } impl<'db> Signature<'db> { - pub(crate) fn new(parameters: Parameters<'db>, return_ty: Type<'db>) -> Self { + pub fn new(parameters: Parameters<'db>, return_ty: Type<'db>) -> Self { Self { generic_context: None, definition: None, @@ -725,7 +725,7 @@ impl<'db> Signature<'db> { } } - pub(crate) fn new_generic( + pub fn new_generic( generic_context: Option>, parameters: Parameters<'db>, return_ty: Type<'db>, @@ -739,7 +739,7 @@ impl<'db> Signature<'db> { } /// Return a signature for a dynamic callable - pub(crate) fn dynamic(signature_type: Type<'db>) -> Self { + pub fn dynamic(signature_type: Type<'db>) -> Self { Signature { generic_context: None, definition: None, @@ -750,7 +750,7 @@ impl<'db> Signature<'db> { /// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo #[allow(unused_variables)] // 'reason' only unused in debug builds - pub(crate) fn todo(reason: &'static str) -> Self { + pub fn todo(reason: &'static str) -> Self { let signature_type = todo_type!(reason); Signature { generic_context: None, @@ -761,7 +761,7 @@ impl<'db> Signature<'db> { } /// Return a typed signature from a function definition. - pub(super) fn from_function( + pub fn from_function( db: &'db dyn Db, pep695_generic_context: Option>, definition: Definition<'db>, @@ -806,19 +806,19 @@ impl<'db> Signature<'db> { } } - pub(super) fn wrap_coroutine_return_type(self, db: &'db dyn Db) -> Self { + pub fn wrap_coroutine_return_type(self, db: &'db dyn Db) -> Self { let return_ty = KnownClass::CoroutineType .to_specialized_instance(db, &[Type::any(), Type::any(), self.return_ty]); Self { return_ty, ..self } } /// Returns the signature which accepts any parameters and returns an `Unknown` type. - pub(crate) fn unknown() -> Self { + pub fn unknown() -> Self { Self::new(Parameters::unknown(), Type::unknown()) } /// Return the "bottom" signature, subtype of all other fully-static signatures. - pub(crate) fn bottom() -> Self { + pub fn bottom() -> Self { Self::new(Parameters::bottom(), Type::Never) } @@ -827,7 +827,7 @@ impl<'db> Signature<'db> { /// `Self` is hidden if it does not appear in: /// 1. The return type /// 2. Any explicitly annotated parameter (not inferred) - pub(crate) fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { + pub fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { !self.return_ty.contains_self(db) && !self .parameters() @@ -835,7 +835,7 @@ impl<'db> Signature<'db> { .any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db)) } - pub(crate) fn with_inherited_generic_context( + pub fn with_inherited_generic_context( mut self, db: &'db dyn Db, inherited_generic_context: GenericContext<'db>, @@ -851,7 +851,7 @@ impl<'db> Signature<'db> { self } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( &self, db: &'db dyn Db, div: Type<'db>, @@ -880,7 +880,7 @@ impl<'db> Signature<'db> { }) } - pub(crate) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -901,7 +901,7 @@ impl<'db> Signature<'db> { } } - pub(crate) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( &self, db: &'db dyn Db, binding_context: Option>, @@ -924,7 +924,7 @@ impl<'db> Signature<'db> { } /// Return the parameters in this signature. - pub(crate) fn parameters(&self) -> &Parameters<'db> { + pub fn parameters(&self) -> &Parameters<'db> { &self.parameters } @@ -933,7 +933,7 @@ impl<'db> Signature<'db> { /// right thing to do! The caller must determine whether the first parameter is actually a /// `self` or `cls` parameter, and must determine the correct type to use as the implicit /// annotation. - pub(crate) fn add_implicit_self_annotation( + pub fn add_implicit_self_annotation( &mut self, db: &'db dyn Db, self_type: impl FnOnce() -> Option>, @@ -980,11 +980,11 @@ impl<'db> Signature<'db> { } /// Return the definition associated with this signature, if any. - pub(crate) fn definition(&self) -> Option> { + pub fn definition(&self) -> Option> { self.definition } - pub(crate) fn bind_self(&self, db: &'db dyn Db, self_type: Option>) -> Self { + pub fn bind_self(&self, db: &'db dyn Db, self_type: Option>) -> Self { let mut parameters = self.parameters.iter().cloned().peekable(); // TODO: Theoretically, for a signature like `f(*args: *tuple[MyClass, int, *tuple[str, ...]])` with @@ -1017,7 +1017,7 @@ impl<'db> Signature<'db> { } } - pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self { + pub fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self { let self_mapping = TypeMapping::BindSelf(SelfBinding::new( db, self_type, @@ -1047,7 +1047,7 @@ impl<'db> Signature<'db> { } } - pub(crate) fn when_constraint_set_assignable_to_signatures<'c>( + pub fn when_constraint_set_assignable_to_signatures<'c>( &self, db: &'db dyn Db, other: &CallableSignature<'db>, @@ -1693,12 +1693,12 @@ impl<'db> Signature<'db> { } /// Create a new signature with the given definition. - pub(crate) fn with_definition(self, definition: Option>) -> Self { + pub fn with_definition(self, definition: Option>) -> Self { Self { definition, ..self } } /// Create a new signature with the given return type. - pub(crate) fn with_return_type(self, return_ty: Type<'db>) -> Self { + pub fn with_return_type(self, return_ty: Type<'db>) -> Self { Self { return_ty, ..self } } } @@ -1733,7 +1733,7 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { /// The kind of parameter list represented. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) enum ParametersKind<'db> { +pub enum ParametersKind<'db> { /// A standard parameter list. #[default] Standard, @@ -1766,7 +1766,7 @@ pub(crate) enum ParametersKind<'db> { } #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) struct Parameters<'db> { +pub struct Parameters<'db> { // TODO: use SmallVec here once invariance bug is fixed value: Vec>, kind: ParametersKind<'db>, @@ -1778,10 +1778,7 @@ impl<'db> Parameters<'db> { /// The kind of the parameter list is determined based on the provided parameters. /// Specifically, if the parameters is made up of `*args` and `**kwargs` only, it checks /// their annotated types to determine if they represent a gradual form or a `ParamSpec`. - pub(crate) fn new( - db: &'db dyn Db, - parameters: impl IntoIterator>, - ) -> Self { + pub fn new(db: &'db dyn Db, parameters: impl IntoIterator>) -> Self { fn new_impl<'db>(db: &'db dyn Db, value: Vec>) -> Parameters<'db> { let mut kind = ParametersKind::Standard; if let [p1, p2] = value.as_slice() @@ -1816,30 +1813,30 @@ impl<'db> Parameters<'db> { } /// Create an empty parameter list. - pub(crate) fn empty() -> Self { + pub fn empty() -> Self { Self { value: Vec::new(), kind: ParametersKind::Standard, } } - pub(crate) fn as_slice(&self) -> &[Parameter<'db>] { + pub fn as_slice(&self) -> &[Parameter<'db>] { self.value.as_slice() } - pub(crate) const fn kind(&self) -> ParametersKind<'db> { + pub const fn kind(&self) -> ParametersKind<'db> { self.kind } - pub(crate) const fn is_gradual(&self) -> bool { + pub const fn is_gradual(&self) -> bool { matches!(self.kind, ParametersKind::Gradual) } - pub(crate) const fn is_top(&self) -> bool { + pub const fn is_top(&self) -> bool { matches!(self.kind, ParametersKind::Top) } - pub(crate) const fn as_paramspec(&self) -> Option> { + pub const fn as_paramspec(&self) -> Option> { match self.kind { ParametersKind::ParamSpec(bound_typevar) => Some(bound_typevar), _ => None, @@ -1847,7 +1844,7 @@ impl<'db> Parameters<'db> { } /// Return todo parameters: (*args: Todo, **kwargs: Todo) - pub(crate) fn todo() -> Self { + pub fn todo() -> Self { Self { value: vec![ Parameter::variadic(Name::new_static("args")) @@ -1864,7 +1861,7 @@ impl<'db> Parameters<'db> { /// Internally, this is represented as `(*Any, **Any)` that accepts parameters of type [`Any`]. /// /// [`Any`]: DynamicType::Any - pub(crate) fn gradual_form() -> Self { + pub fn gradual_form() -> Self { Self { value: vec![ Parameter::variadic(Name::new_static("args")) @@ -1876,7 +1873,7 @@ impl<'db> Parameters<'db> { } } - pub(crate) fn paramspec(db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> Self { + pub fn paramspec(db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> Self { Self { value: vec![ Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::TypeVar( @@ -1896,7 +1893,7 @@ impl<'db> Parameters<'db> { /// [`Unknown`]. /// /// [`Unknown`]: crate::types::DynamicType::Unknown - pub(crate) fn unknown() -> Self { + pub fn unknown() -> Self { Self { value: vec![ Parameter::variadic(Name::new_static("args")) @@ -1910,7 +1907,7 @@ impl<'db> Parameters<'db> { /// Return parameters that represents `(*args: object, **kwargs: object)`, the bottom signature /// (accepts any call, so subtype of all other signatures.) - pub(crate) fn bottom() -> Self { + pub fn bottom() -> Self { Self { value: vec![ Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object()), @@ -1926,7 +1923,7 @@ impl<'db> Parameters<'db> { /// signatures. This is not `(*Never, **Never)`, which is equivalent to no parameters at all /// and still accepts the empty call `()`; it has to be represented instead as a special /// `ParametersKind`. - pub(crate) fn top() -> Self { + pub fn top() -> Self { Self { // We always emit `called-top-callable` for any call to the top callable (based on the // `kind` below), so we otherwise give it the most permissive signature`(*object, @@ -1941,7 +1938,7 @@ impl<'db> Parameters<'db> { } /// Returns the bound `ParamSpec` type variable if the parameters contain a `ParamSpec`. - pub(crate) fn find_paramspec_from_args_kwargs<'a>( + pub fn find_paramspec_from_args_kwargs<'a>( &'a self, db: &'db dyn Db, ) -> Option<(&'a [Parameter<'db>], BoundTypeVarInstance<'db>)> { @@ -2129,11 +2126,11 @@ impl<'db> Parameters<'db> { } } - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { self.value.len() } - pub(crate) fn iter(&self) -> std::slice::Iter<'_, Parameter<'db>> { + pub fn iter(&self) -> std::slice::Iter<'_, Parameter<'db>> { self.value.iter() } @@ -2142,25 +2139,25 @@ impl<'db> Parameters<'db> { /// For a valid signature, this will be all positional parameters. In an invalid signature, /// there could be non-initial positional parameters; effectively, we just won't consider those /// to be positional, which is fine. - pub(crate) fn positional(&self) -> impl Iterator> { + pub fn positional(&self) -> impl Iterator> { self.iter().take_while(|param| param.is_positional()) } /// Return parameter at given index, or `None` if index is out-of-range. - pub(crate) fn get(&self, index: usize) -> Option<&Parameter<'db>> { + pub fn get(&self, index: usize) -> Option<&Parameter<'db>> { self.value.get(index) } /// Return positional parameter at given index, or `None` if `index` is out of range. /// /// Does not return variadic parameter. - pub(crate) fn get_positional(&self, index: usize) -> Option<&Parameter<'db>> { + pub fn get_positional(&self, index: usize) -> Option<&Parameter<'db>> { self.get(index) .and_then(|parameter| parameter.is_positional().then_some(parameter)) } /// Return a positional-only parameter (with index) with the given name. - pub(crate) fn positional_only_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> { + pub fn positional_only_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> { self.iter().enumerate().find(|(_, parameter)| { parameter.is_positional_only() && parameter @@ -2171,7 +2168,7 @@ impl<'db> Parameters<'db> { } /// Return the variadic parameter (`*args`), if any, and its index, or `None`. - pub(crate) fn variadic(&self) -> Option<(usize, &Parameter<'db>)> { + pub fn variadic(&self) -> Option<(usize, &Parameter<'db>)> { self.iter() .enumerate() .find(|(_, parameter)| parameter.is_variadic()) @@ -2183,14 +2180,14 @@ impl<'db> Parameters<'db> { /// /// In an invalid signature, there could be multiple parameters with the same name; we will /// just return the first that matches. - pub(crate) fn keyword_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> { + pub fn keyword_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> { self.iter() .enumerate() .find(|(_, parameter)| parameter.callable_by_name(name)) } /// Return the keywords parameter (`**kwargs`), if any, and its index, or `None`. - pub(crate) fn keyword_variadic(&self) -> Option<(usize, &Parameter<'db>)> { + pub fn keyword_variadic(&self) -> Option<(usize, &Parameter<'db>)> { self.iter() .enumerate() .rfind(|(_, parameter)| parameter.is_keyword_variadic()) @@ -2215,7 +2212,7 @@ impl<'db> std::ops::Index for Parameters<'db> { } #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) struct Parameter<'db> { +pub struct Parameter<'db> { /// Annotated type of the parameter. If no annotation was provided, this is `Unknown`. annotated_type: Type<'db>, @@ -2224,7 +2221,7 @@ pub(crate) struct Parameter<'db> { /// parameter of instance method, or `type[Self]` for `cls` parameter of classmethods. This /// field is only used to decide whether to display the annotated type; it has no effect on the /// type semantics of the parameter. - pub(crate) inferred_annotation: bool, + pub inferred_annotation: bool, /// Variadic parameters can have starred annotations, e.g. /// - `*args: *Ts` @@ -2236,11 +2233,11 @@ pub(crate) struct Parameter<'db> { has_starred_annotation: bool, kind: ParameterKind<'db>, - pub(crate) form: ParameterForm, + pub form: ParameterForm, } impl<'db> Parameter<'db> { - pub(crate) fn positional_only(name: Option) -> Self { + pub fn positional_only(name: Option) -> Self { Self { annotated_type: Type::unknown(), inferred_annotation: true, @@ -2253,7 +2250,7 @@ impl<'db> Parameter<'db> { } } - pub(crate) fn positional_or_keyword(name: Name) -> Self { + pub fn positional_or_keyword(name: Name) -> Self { Self { annotated_type: Type::unknown(), inferred_annotation: true, @@ -2266,7 +2263,7 @@ impl<'db> Parameter<'db> { } } - pub(crate) fn variadic(name: Name) -> Self { + pub fn variadic(name: Name) -> Self { Self { annotated_type: Type::unknown(), inferred_annotation: true, @@ -2276,7 +2273,7 @@ impl<'db> Parameter<'db> { } } - pub(crate) fn keyword_only(name: Name) -> Self { + pub fn keyword_only(name: Name) -> Self { Self { annotated_type: Type::unknown(), inferred_annotation: true, @@ -2289,7 +2286,7 @@ impl<'db> Parameter<'db> { } } - pub(crate) fn keyword_variadic(name: Name) -> Self { + pub fn keyword_variadic(name: Name) -> Self { Self { annotated_type: Type::unknown(), inferred_annotation: true, @@ -2301,13 +2298,13 @@ impl<'db> Parameter<'db> { /// Set the annotated type for this parameter. This also marks the annotation as explicit /// (not inferred), so it will be displayed. - pub(crate) fn with_annotated_type(mut self, annotated_type: Type<'db>) -> Self { + pub fn with_annotated_type(mut self, annotated_type: Type<'db>) -> Self { self.annotated_type = annotated_type; self.inferred_annotation = false; self } - pub(crate) fn with_default_type(mut self, default: Type<'db>) -> Self { + pub fn with_default_type(mut self, default: Type<'db>) -> Self { match &mut self.kind { ParameterKind::PositionalOnly { default_type, .. } | ParameterKind::PositionalOrKeyword { default_type, .. } @@ -2319,7 +2316,7 @@ impl<'db> Parameter<'db> { self } - pub(crate) fn with_optional_default_type(self, default: Option>) -> Self { + pub fn with_optional_default_type(self, default: Option>) -> Self { if let Some(default) = default { self.with_default_type(default) } else { @@ -2327,7 +2324,7 @@ impl<'db> Parameter<'db> { } } - pub(crate) fn type_form(mut self) -> Self { + pub fn type_form(mut self) -> Self { self.form = ParameterForm::Type; self } @@ -2355,7 +2352,7 @@ impl<'db> Parameter<'db> { } } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( &self, db: &'db dyn Db, div: Type<'db>, @@ -2456,35 +2453,35 @@ impl<'db> Parameter<'db> { } /// Returns `true` if this is a keyword-only parameter. - pub(crate) fn is_keyword_only(&self) -> bool { + pub fn is_keyword_only(&self) -> bool { matches!(self.kind, ParameterKind::KeywordOnly { .. }) } /// Returns `true` if this is a positional-only parameter. - pub(crate) fn is_positional_only(&self) -> bool { + pub fn is_positional_only(&self) -> bool { matches!(self.kind, ParameterKind::PositionalOnly { .. }) } /// Returns `true` if this is a variadic parameter. - pub(crate) fn is_variadic(&self) -> bool { + pub fn is_variadic(&self) -> bool { matches!(self.kind, ParameterKind::Variadic { .. }) } /// Returns `true` if this is a keyword-variadic parameter. - pub(crate) fn is_keyword_variadic(&self) -> bool { + pub fn is_keyword_variadic(&self) -> bool { matches!(self.kind, ParameterKind::KeywordVariadic { .. }) } /// Returns `true` if this is either a positional-only or standard (positional or keyword) /// parameter. - pub(crate) fn is_positional(&self) -> bool { + pub fn is_positional(&self) -> bool { matches!( self.kind, ParameterKind::PositionalOnly { .. } | ParameterKind::PositionalOrKeyword { .. } ) } - pub(crate) fn callable_by_name(&self, name: &str) -> bool { + pub fn callable_by_name(&self, name: &str) -> bool { match &self.kind { ParameterKind::PositionalOrKeyword { name: param_name, .. @@ -2497,28 +2494,28 @@ impl<'db> Parameter<'db> { } /// Annotated type of the parameter. If no annotation was provided, this is `Unknown`. - pub(crate) fn annotated_type(&self) -> Type<'db> { + pub fn annotated_type(&self) -> Type<'db> { self.annotated_type } /// Return `true` if this parameter has a starred annotation, /// e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...], bytes]` - pub(crate) fn has_starred_annotation(&self) -> bool { + pub fn has_starred_annotation(&self) -> bool { self.has_starred_annotation } /// Kind of the parameter. - pub(crate) fn kind(&self) -> &ParameterKind<'db> { + pub fn kind(&self) -> &ParameterKind<'db> { &self.kind } /// Whether or not the type of this parameter should be displayed. - pub(crate) fn should_annotation_be_displayed(&self) -> bool { + pub fn should_annotation_be_displayed(&self) -> bool { !self.inferred_annotation } /// Name of the parameter (if it has one). - pub(crate) fn name(&self) -> Option<&ast::name::Name> { + pub fn name(&self) -> Option<&ast::name::Name> { match &self.kind { ParameterKind::PositionalOnly { name, .. } => name.as_ref(), ParameterKind::PositionalOrKeyword { name, .. } => Some(name), @@ -2529,7 +2526,7 @@ impl<'db> Parameter<'db> { } /// Display name of the parameter, if it has one. - pub(crate) fn display_name(&self) -> Option { + pub fn display_name(&self) -> Option { self.name().map(|name| match self.kind { ParameterKind::Variadic { .. } => ast::name::Name::new(format!("*{name}")), ParameterKind::KeywordVariadic { .. } => ast::name::Name::new(format!("**{name}")), @@ -2538,7 +2535,7 @@ impl<'db> Parameter<'db> { } /// Default-value type of the parameter, if any. - pub(crate) fn default_type(&self) -> Option> { + pub fn default_type(&self) -> Option> { match self.kind { ParameterKind::PositionalOnly { default_type, .. } | ParameterKind::PositionalOrKeyword { default_type, .. } @@ -2625,7 +2622,7 @@ impl<'db> ParameterKind<'db> { /// Whether a parameter is used as a value or a type form. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] -pub(crate) enum ParameterForm { +pub enum ParameterForm { Value, Type, } diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 7a0966c3ac4c7..af1f3e53e7673 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -118,7 +118,7 @@ pub enum SpecialFormType { impl SpecialFormType { /// Return the [`KnownClass`] which this symbol is an instance of - pub(crate) const fn class(self) -> KnownClass { + pub const fn class(self) -> KnownClass { match self { Self::Annotated | Self::Literal @@ -166,20 +166,16 @@ impl SpecialFormType { /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`, /// so `SpecialFormType::Literal.instance_fallback(db)` /// returns `Type::NominalInstance(NominalInstanceType { class: })`. - pub(super) fn instance_fallback(self, db: &dyn Db) -> Type<'_> { + pub fn instance_fallback(self, db: &dyn Db) -> Type<'_> { self.class().to_instance(db) } /// Return `true` if this symbol is an instance of `class`. - pub(super) fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { + pub fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { self.class().is_subclass_of(db, class) } - pub(super) fn try_from_file_and_name( - db: &dyn Db, - file: File, - symbol_name: &str, - ) -> Option { + pub fn try_from_file_and_name(db: &dyn Db, file: File, symbol_name: &str) -> Option { let candidate = Self::from_name(symbol_name)?; candidate .check_module(file_to_module(db, file)?.known(db)?) @@ -366,7 +362,7 @@ impl SpecialFormType { /// /// Most variants can only exist in one module, which is the same as `self.class().canonical_module(db)`. /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. - pub(super) fn check_module(self, module: KnownModule) -> bool { + pub fn check_module(self, module: KnownModule) -> bool { match self { Self::TypeQualifier(TypeQualifier::ClassVar) | Self::LegacyStdlibAlias(_) @@ -413,14 +409,14 @@ impl SpecialFormType { } } - pub(super) fn to_meta_type(self, db: &dyn Db) -> Type<'_> { + pub fn to_meta_type(self, db: &dyn Db) -> Type<'_> { self.class().to_class_literal(db) } /// Return true if this special form is callable at runtime. /// Most special forms are not callable (they are type constructors that are subscripted), /// but some like `TypedDict` and collection constructors can be called. - pub(super) const fn is_callable(self) -> bool { + pub const fn is_callable(self) -> bool { match self { // TypedDict can be called as a constructor to create TypedDict types Self::TypedDict @@ -480,7 +476,7 @@ impl SpecialFormType { /// Return `true` if this special form is valid as the second argument /// to `issubclass()` and `isinstance()` calls. - pub(super) const fn is_valid_isinstance_target(self) -> bool { + pub const fn is_valid_isinstance_target(self) -> bool { match self { Self::Callable | Self::LegacyStdlibAlias(_) @@ -519,7 +515,7 @@ impl SpecialFormType { } /// Return the name of the symbol at runtime - pub(super) const fn name(self) -> &'static str { + pub const fn name(self) -> &'static str { match self { SpecialFormType::Any => "Any", SpecialFormType::Annotated => "Annotated", @@ -609,7 +605,7 @@ impl SpecialFormType { } } - pub(super) fn definition(self, db: &dyn Db) -> Option> { + pub fn definition(self, db: &dyn Db) -> Option> { self.definition_modules() .iter() .find_map(|module| { @@ -630,7 +626,7 @@ impl SpecialFormType { /// /// This is called for the "misc" special forms that are not aliases, type qualifiers, /// `Tuple`, `Type`, or `Callable` (those are handled by their respective call sites). - pub(super) fn in_type_expression<'db>( + pub fn in_type_expression<'db>( self, db: &'db dyn Db, scope_id: ScopeId<'db>, @@ -773,7 +769,7 @@ pub enum LegacyStdlibAlias { } impl LegacyStdlibAlias { - pub(super) const fn alias_spec(self) -> AliasSpec { + pub const fn alias_spec(self) -> AliasSpec { let (class, expected_argument_number) = match self { LegacyStdlibAlias::List => (KnownClass::List, 1), LegacyStdlibAlias::Dict => (KnownClass::Dict, 2), @@ -792,7 +788,7 @@ impl LegacyStdlibAlias { } } - pub(super) const fn aliased_class(self) -> KnownClass { + pub const fn aliased_class(self) -> KnownClass { self.alias_spec().class } } @@ -844,7 +840,7 @@ impl std::fmt::Display for TypeQualifier { /// Information regarding the [`KnownClass`] a [`LegacyStdlibAlias`] refers to. #[derive(Debug)] -pub(super) struct AliasSpec { - pub(super) class: KnownClass, - pub(super) expected_argument_number: usize, +pub struct AliasSpec { + pub class: KnownClass, + pub expected_argument_number: usize, } diff --git a/crates/ty_python_semantic/src/types/string_annotation.rs b/crates/ty_python_semantic/src/types/string_annotation.rs index d13e8a4ca64a9..9779029e1edb5 100644 --- a/crates/ty_python_semantic/src/types/string_annotation.rs +++ b/crates/ty_python_semantic/src/types/string_annotation.rs @@ -27,7 +27,7 @@ declare_lint! { /// def test(): -> "int": /// ... /// ``` - pub(crate) static FSTRING_TYPE_ANNOTATION = { + pub static FSTRING_TYPE_ANNOTATION = { summary: "detects F-strings in type annotation positions", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -52,7 +52,7 @@ declare_lint! { /// def test(): -> "int": /// ... /// ``` - pub(crate) static BYTE_STRING_TYPE_ANNOTATION = { + pub static BYTE_STRING_TYPE_ANNOTATION = { summary: "detects byte strings in type annotation positions", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -77,7 +77,7 @@ declare_lint! { /// def test(): -> "int": /// ... /// ``` - pub(crate) static RAW_STRING_TYPE_ANNOTATION = { + pub static RAW_STRING_TYPE_ANNOTATION = { summary: "detects raw strings in type annotation positions", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -102,7 +102,7 @@ declare_lint! { /// def test(): -> "Literal[5]": /// ... /// ``` - pub(crate) static IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION = { + pub static IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION = { summary: "detects implicit concatenated strings in type annotations", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -145,7 +145,7 @@ declare_lint! { /// ## References /// - [Typing spec: The meaning of annotations](https://typing.python.org/en/latest/spec/annotations.html#the-meaning-of-annotations) /// - [Typing spec: String annotations](https://typing.python.org/en/latest/spec/annotations.html#string-annotations) - pub(crate) static INVALID_SYNTAX_IN_FORWARD_ANNOTATION = { + pub static INVALID_SYNTAX_IN_FORWARD_ANNOTATION = { summary: "detects invalid syntax in forward annotations", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -164,7 +164,7 @@ declare_lint! { /// ```python /// def foo() -> "intt\b": ... /// ``` - pub(crate) static ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION = { + pub static ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION = { summary: "detects forward type annotations with escape characters", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, @@ -172,7 +172,7 @@ declare_lint! { } /// Parses the given expression as a string annotation. -pub(crate) fn parse_string_annotation( +pub fn parse_string_annotation( context: &InferContext, string_expr: &ast::ExprStringLiteral, ) -> Option> { diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index c8e0e8fadcc4a..cac913d110463 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -21,7 +21,7 @@ pub struct SubclassOfType<'db> { subclass_of: SubclassOfInner<'db>, } -pub(super) fn walk_subclass_of_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_subclass_of_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, subclass_of: SubclassOfType<'db>, visitor: &V, @@ -41,7 +41,7 @@ impl<'db> SubclassOfType<'db> { /// /// The eager normalization here means that we do not need to worry elsewhere about distinguishing /// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants. - pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { + pub fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { let subclass_of = subclass_of.into(); match subclass_of { SubclassOfInner::Class(class) => { @@ -60,7 +60,7 @@ impl<'db> SubclassOfType<'db> { } /// Given the class object `T`, returns a [`Type`] instance representing `type[T]`. - pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option> { + pub fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option> { let subclass_of = match ty { Type::Dynamic(dynamic) => SubclassOfInner::Dynamic(dynamic), Type::ClassLiteral(literal) => { @@ -78,7 +78,7 @@ impl<'db> SubclassOfType<'db> { } /// Given an instance of the class or type variable `T`, returns a [`Type`] instance representing `type[T]`. - pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option> { + pub fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option> { // Handle unions by distributing `type[]` over each element: // `type[A | B]` -> `type[A] | type[B]` if let Type::Union(union) = ty { @@ -95,7 +95,7 @@ impl<'db> SubclassOfType<'db> { } /// Return a [`Type`] instance representing the type `type[Unknown]`. - pub(crate) const fn subclass_of_unknown() -> Type<'db> { + pub const fn subclass_of_unknown() -> Type<'db> { Type::SubclassOf(SubclassOfType { subclass_of: SubclassOfInner::unknown(), }) @@ -103,30 +103,30 @@ impl<'db> SubclassOfType<'db> { /// Return a [`Type`] instance representing the type `type[Any]`. #[cfg(test)] - pub(crate) const fn subclass_of_any() -> Type<'db> { + pub const fn subclass_of_any() -> Type<'db> { Type::SubclassOf(SubclassOfType { subclass_of: SubclassOfInner::Dynamic(DynamicType::Any), }) } /// Return a [`Type`] instance representing the type `type[object]`. - pub(crate) fn subclass_of_object(db: &'db dyn Db) -> Type<'db> { + pub fn subclass_of_object(db: &'db dyn Db) -> Type<'db> { // See the documentation of `SubclassOfType::from` for details. KnownClass::Type.to_instance(db) } /// Return the inner [`SubclassOfInner`] value wrapped by this `SubclassOfType`. - pub(crate) const fn subclass_of(self) -> SubclassOfInner<'db> { + pub const fn subclass_of(self) -> SubclassOfInner<'db> { self.subclass_of } - pub(crate) const fn is_dynamic(self) -> bool { + pub const fn is_dynamic(self) -> bool { // Unpack `self` so that we're forced to update this method if any more fields are added in the future. let Self { subclass_of } = self; subclass_of.is_dynamic() } - pub(crate) const fn is_type_var(self) -> bool { + pub const fn is_type_var(self) -> bool { let Self { subclass_of } = self; subclass_of.is_type_var() } @@ -135,7 +135,7 @@ impl<'db> SubclassOfType<'db> { self.subclass_of.into_type_var() } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -170,7 +170,7 @@ impl<'db> SubclassOfType<'db> { } } - pub(super) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -193,7 +193,7 @@ impl<'db> SubclassOfType<'db> { } } - pub(crate) fn find_name_in_mro_with_policy( + pub fn find_name_in_mro_with_policy( self, db: &'db dyn Db, name: &str, @@ -218,7 +218,7 @@ impl<'db> SubclassOfType<'db> { /// Return `true` if `self` has a certain relation to `other`. #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: SubclassOfType<'db>, @@ -265,7 +265,7 @@ impl<'db> SubclassOfType<'db> { /// Return` true` if `self` is a disjoint type from `other`. /// /// See [`Type::is_disjoint_from`] for more details. - pub(crate) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -289,7 +289,7 @@ impl<'db> SubclassOfType<'db> { } } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -302,7 +302,7 @@ impl<'db> SubclassOfType<'db> { }) } - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_instance(self, db: &'db dyn Db) -> Type<'db> { match self.subclass_of { SubclassOfInner::Class(class) => Type::instance(db, class), SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), @@ -315,7 +315,7 @@ impl<'db> SubclassOfType<'db> { /// For `type[C]` where `C` is a concrete class, this returns `type[metaclass(C)]`. /// For `type[T]` where `T` is a `TypeVar`, this computes the metatype based on the /// `TypeVar`'s bounds or constraints. - pub(crate) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { match self.subclass_of.with_transposed_type_var(db) { SubclassOfInner::Dynamic(dynamic) => { SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)) @@ -339,7 +339,7 @@ impl<'db> SubclassOfType<'db> { } } - pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool { + pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { self.subclass_of .into_class(db) .is_some_and(|class| class.class_literal(db).is_typed_dict(db)) @@ -371,26 +371,26 @@ impl<'db> VarianceInferable<'db> for SubclassOfType<'db> { /// but does not include the `ClassBase::Protocol` and `ClassBase::Generic` variants /// (`type[Protocol]` and `type[Generic]` are not valid types). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) enum SubclassOfInner<'db> { +pub enum SubclassOfInner<'db> { Class(ClassType<'db>), Dynamic(DynamicType<'db>), TypeVar(BoundTypeVarInstance<'db>), } impl<'db> SubclassOfInner<'db> { - pub(crate) const fn unknown() -> Self { + pub const fn unknown() -> Self { Self::Dynamic(DynamicType::Unknown) } - pub(crate) const fn is_dynamic(self) -> bool { + pub const fn is_dynamic(self) -> bool { matches!(self, Self::Dynamic(_)) } - pub(crate) const fn is_type_var(self) -> bool { + pub const fn is_type_var(self) -> bool { matches!(self, Self::TypeVar(_)) } - pub(crate) fn into_class(self, db: &'db dyn Db) -> Option> { + pub fn into_class(self, db: &'db dyn Db) -> Option> { match self { Self::Dynamic(_) => None, Self::Class(class) => Some(class), @@ -408,21 +408,21 @@ impl<'db> SubclassOfInner<'db> { } } - pub(crate) const fn into_dynamic(self) -> Option> { + pub const fn into_dynamic(self) -> Option> { match self { Self::Class(_) | Self::TypeVar(_) => None, Self::Dynamic(dynamic) => Some(dynamic), } } - pub(crate) const fn into_type_var(self) -> Option> { + pub const fn into_type_var(self) -> Option> { match self { Self::Class(_) | Self::Dynamic(_) => None, Self::TypeVar(bound_typevar) => Some(bound_typevar), } } - pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option { + pub fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option { Some(match ty { Type::NominalInstance(instance) => SubclassOfInner::Class(instance.class(db)), Type::TypedDict(typed_dict) => match typed_dict { @@ -449,7 +449,7 @@ impl<'db> SubclassOfInner<'db> { /// - Otherwise, for an unbounded type variable, this returns `type[object]`. /// /// If this is type of a concrete type `C`, returns the type unchanged. - pub(crate) fn with_transposed_type_var(self, db: &'db dyn Db) -> Self { + pub fn with_transposed_type_var(self, db: &'db dyn Db) -> Self { let Some(bound_typevar) = self.into_type_var() else { return self; }; @@ -478,7 +478,7 @@ impl<'db> SubclassOfInner<'db> { Self::TypeVar(bound_typevar) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index 994fecc1ba461..035a7a66a06a8 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -28,14 +28,14 @@ use super::{ /// The kind of subscriptable type that had an out-of-bounds index. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum SubscriptKind { +pub enum SubscriptKind { Tuple, String, BytesLiteral, } impl SubscriptKind { - pub(crate) const fn as_str(self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { Self::Tuple => "tuple", Self::String => "string", @@ -52,7 +52,7 @@ impl Display for SubscriptKind { /// A dunder method used for subscripting. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum DunderMethod { +pub enum DunderMethod { GetItem, ClassGetItem, } @@ -64,7 +64,7 @@ impl Display for DunderMethod { } impl DunderMethod { - pub(crate) const fn as_str(self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { Self::GetItem => "__getitem__", Self::ClassGetItem => "__class_getitem__", @@ -74,7 +74,7 @@ impl DunderMethod { /// The origin of a legacy generic subscription (`Generic` or `Protocol`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum LegacyGenericOrigin { +pub enum LegacyGenericOrigin { Generic, Protocol, } @@ -89,13 +89,13 @@ impl Display for LegacyGenericOrigin { } #[derive(Debug)] -pub(crate) struct SubscriptError<'db> { +pub struct SubscriptError<'db> { result_ty: Type<'db>, errors: Vec>, } #[derive(Debug)] -pub(crate) enum SubscriptErrorKind<'db> { +pub enum SubscriptErrorKind<'db> { /// An index is out of bounds for a literal tuple/string/bytes subscript. IndexOutOfBounds { kind: SubscriptKind, @@ -140,7 +140,7 @@ pub(crate) enum SubscriptErrorKind<'db> { } impl<'db> SubscriptError<'db> { - pub(crate) fn new(result_ty: Type<'db>, error: SubscriptErrorKind<'db>) -> Self { + pub fn new(result_ty: Type<'db>, error: SubscriptErrorKind<'db>) -> Self { Self { result_ty, errors: vec![error], @@ -151,7 +151,7 @@ impl<'db> SubscriptError<'db> { Self { result_ty, errors } } - pub(crate) fn result_type(&self) -> Type<'db> { + pub fn result_type(&self) -> Type<'db> { self.result_ty } @@ -164,7 +164,7 @@ impl<'db> SubscriptError<'db> { self.errors.iter().any(SubscriptErrorKind::method_available) } - pub(crate) fn report_diagnostics( + pub fn report_diagnostics( &self, context: &InferContext<'db, '_>, subscript: &ast::ExprSubscript, @@ -412,7 +412,7 @@ where } impl<'db> Type<'db> { - pub(super) fn subscript( + pub fn subscript( self, db: &'db dyn Db, slice_ty: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 4c602584db19d..d0fba40c99a6b 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -39,23 +39,23 @@ use crate::types::{Truthiness, TypeContext}; use crate::{Db, FxOrderSet, Program}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum TupleLength { +pub enum TupleLength { Fixed(usize), Variable(usize, usize), } impl TupleLength { - pub(crate) const fn unknown() -> TupleLength { + pub const fn unknown() -> TupleLength { TupleLength::Variable(0, 0) } - pub(crate) const fn is_variable(self) -> bool { + pub const fn is_variable(self) -> bool { matches!(self, TupleLength::Variable(_, _)) } /// Returns the minimum and maximum length of this tuple. (The maximum length will be `None` /// for a tuple with a variable-length portion.) - pub(crate) fn size_hint(self) -> (usize, Option) { + pub fn size_hint(self) -> (usize, Option) { match self { TupleLength::Fixed(len) => (len, Some(len)), TupleLength::Variable(prefix, suffix) => (prefix + suffix, None), @@ -63,7 +63,7 @@ impl TupleLength { } /// Returns the minimum length of this tuple. - pub(crate) fn minimum(self) -> usize { + pub fn minimum(self) -> usize { match self { TupleLength::Fixed(len) => len, TupleLength::Variable(prefix, suffix) => prefix + suffix, @@ -71,7 +71,7 @@ impl TupleLength { } /// Returns the maximum length of this tuple, if any. - pub(crate) fn maximum(self) -> Option { + pub fn maximum(self) -> Option { match self { TupleLength::Fixed(len) => Some(len), TupleLength::Variable(_, _) => None, @@ -80,7 +80,7 @@ impl TupleLength { /// Given two [`TupleLength`]s, return the more precise instance, /// if it makes sense to consider one more precise than the other. - pub(crate) fn most_precise(self, other: Self) -> Option { + pub fn most_precise(self, other: Self) -> Option { match (self, other) { // A fixed-length tuple is equally as precise as another fixed-length tuple if they // have the same length. For two differently sized fixed-length tuples, however, @@ -105,7 +105,7 @@ impl TupleLength { } } - pub(crate) fn display_minimum(self) -> String { + pub fn display_minimum(self) -> String { let minimum_length = self.minimum(); match self { TupleLength::Fixed(_) => minimum_length.to_string(), @@ -113,14 +113,14 @@ impl TupleLength { } } - pub(crate) fn display_maximum(self) -> String { + pub fn display_maximum(self) -> String { match self.maximum() { Some(maximum) => maximum.to_string(), None => "unlimited".to_string(), } } - pub(crate) fn into_fixed_length(self) -> Option { + pub fn into_fixed_length(self) -> Option { match self { TupleLength::Fixed(len) => Some(len), TupleLength::Variable(_, _) => None, @@ -131,10 +131,10 @@ impl TupleLength { #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] pub struct TupleType<'db> { #[returns(ref)] - pub(crate) tuple: TupleSpec<'db>, + pub tuple: TupleSpec<'db>, } -pub(super) fn walk_tuple_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_tuple_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, tuple: TupleType<'db>, visitor: &V, @@ -149,7 +149,7 @@ impl get_size2::GetSize for TupleType<'_> {} #[salsa::tracked] impl<'db> TupleType<'db> { - pub(crate) fn new(db: &'db dyn Db, spec: &TupleSpec<'db>) -> Option { + pub fn new(db: &'db dyn Db, spec: &TupleSpec<'db>) -> Option { // If a fixed-length (i.e., mandatory) element of the tuple is `Never`, then it's not // possible to instantiate the tuple as a whole. if spec.fixed_elements().any(Type::is_never) { @@ -172,11 +172,11 @@ impl<'db> TupleType<'db> { Some(TupleType::new_internal(db, spec)) } - pub(crate) fn empty(db: &'db dyn Db) -> Self { + pub fn empty(db: &'db dyn Db) -> Self { TupleType::new_internal(db, TupleSpec::from(FixedLengthTuple::empty())) } - pub(crate) fn heterogeneous( + pub fn heterogeneous( db: &'db dyn Db, types: impl IntoIterator>, ) -> Option { @@ -184,7 +184,7 @@ impl<'db> TupleType<'db> { } #[cfg(test)] - pub(crate) fn mixed( + pub fn mixed( db: &'db dyn Db, prefix: impl IntoIterator>, variable: Type<'db>, @@ -193,7 +193,7 @@ impl<'db> TupleType<'db> { TupleType::new(db, &VariableLengthTuple::mixed(prefix, variable, suffix)) } - pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Self { + pub fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Self { match element { Type::Never => TupleType::empty(db), _ => TupleType::new_internal(db, TupleSpec::homogeneous(element)), @@ -204,7 +204,7 @@ impl<'db> TupleType<'db> { // `static-frame` as part of a mypy_primer run! This is because it's called // from `NominalInstanceType::class()`, which is a very hot method. #[salsa::tracked(cycle_initial=to_class_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn to_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + pub fn to_class_type(self, db: &'db dyn Db) -> ClassType<'db> { let tuple_class = KnownClass::Tuple .try_to_class_literal(db) .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); @@ -219,7 +219,7 @@ impl<'db> TupleType<'db> { }) } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -232,7 +232,7 @@ impl<'db> TupleType<'db> { )) } - pub(crate) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -247,7 +247,7 @@ impl<'db> TupleType<'db> { ) } - pub(crate) fn find_legacy_typevars_impl( + pub fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -259,7 +259,7 @@ impl<'db> TupleType<'db> { } #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -280,7 +280,7 @@ impl<'db> TupleType<'db> { ) } - pub(crate) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: Self, @@ -299,7 +299,7 @@ impl<'db> TupleType<'db> { ) } - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { + pub fn is_single_valued(self, db: &'db dyn Db) -> bool { self.tuple(db).is_single_valued(db) } } @@ -327,7 +327,7 @@ fn to_class_type_cycle_initial<'db>( /// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a /// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that /// must contain an element that can't be instantiated, can't be instantiated itself). -pub(crate) type TupleSpec<'db> = Tuple>; +pub type TupleSpec<'db> = Tuple>; /// A fixed-length tuple. /// @@ -345,31 +345,31 @@ impl FixedLengthTuple { Self(elements.into_iter().collect()) } - pub(crate) fn elements_slice(&self) -> &[T] { + pub fn elements_slice(&self) -> &[T] { &self.0 } - pub(crate) fn owned_elements(self) -> Box<[T]> { + pub fn owned_elements(self) -> Box<[T]> { self.0 } - pub(crate) fn all_elements(&self) -> &[T] { + pub fn all_elements(&self) -> &[T] { &self.0 } - pub(crate) fn iter_all_elements(&self) -> impl DoubleEndedIterator + pub fn iter_all_elements(&self) -> impl DoubleEndedIterator where T: Copy, { self.0.iter().copied() } - pub(crate) fn into_all_elements_with_kind(self) -> impl Iterator> { + pub fn into_all_elements_with_kind(self) -> impl Iterator> { self.0.into_iter().map(TupleElement::Fixed) } /// Returns the length of this tuple. - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { self.0.len() } } @@ -609,7 +609,7 @@ impl<'db> PySlice<'db> for FixedLengthTuple> { /// types, use [`TupleSpec`], which defines some additional type-specific methods. #[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] pub struct VariableLengthTuple { - pub(crate) elements: smallvec::SmallVec<[T; 1]>, + pub elements: smallvec::SmallVec<[T; 1]>, variable_index: usize, } @@ -697,48 +697,48 @@ impl VariableLengthTuple { } } - pub(crate) fn variable(&self) -> T + pub fn variable(&self) -> T where T: Copy, { self.elements[self.variable_index] } - pub(crate) fn variable_element(&self) -> &T { + pub fn variable_element(&self) -> &T { &self.elements[self.variable_index] } - pub(crate) fn variable_element_mut(&mut self) -> &mut T { + pub fn variable_element_mut(&mut self) -> &mut T { &mut self.elements[self.variable_index] } - pub(crate) fn prefix_elements(&self) -> &[T] { + pub fn prefix_elements(&self) -> &[T] { &self.elements[..self.variable_index] } - pub(crate) fn iter_prefix_elements(&self) -> impl DoubleEndedIterator + pub fn iter_prefix_elements(&self) -> impl DoubleEndedIterator where T: Copy, { self.prefix_elements().iter().copied() } - pub(crate) fn prefix_elements_mut(&mut self) -> &mut [T] { + pub fn prefix_elements_mut(&mut self) -> &mut [T] { &mut self.elements[..self.variable_index] } - pub(crate) fn suffix_elements(&self) -> &[T] { + pub fn suffix_elements(&self) -> &[T] { &self.elements[self.suffix_offset()..] } - pub(crate) fn iter_suffix_elements(&self) -> impl DoubleEndedIterator + pub fn iter_suffix_elements(&self) -> impl DoubleEndedIterator where T: Copy, { self.suffix_elements().iter().copied() } - pub(crate) fn suffix_elements_mut(&mut self) -> &mut [T] { + pub fn suffix_elements_mut(&mut self) -> &mut [T] { let suffix_offset = self.suffix_offset(); &mut self.elements[suffix_offset..] } @@ -1248,27 +1248,27 @@ pub enum Tuple { impl Tuple { /// Returns the inner fixed-length tuple if this is a `Tuple::Fixed` variant. - pub(crate) fn as_fixed_length(&self) -> Option<&FixedLengthTuple> { + pub fn as_fixed_length(&self) -> Option<&FixedLengthTuple> { match self { Tuple::Fixed(tuple) => Some(tuple), Tuple::Variable(_) => None, } } - pub(crate) const fn is_variadic(&self) -> bool { + pub const fn is_variadic(&self) -> bool { matches!(self, Tuple::Variable(_)) } - pub(crate) const fn homogeneous(element: T) -> Self { + pub const fn homogeneous(element: T) -> Self { Self::Variable(VariableLengthTuple::homogeneous(element)) } - pub(crate) fn heterogeneous(elements: impl IntoIterator) -> Self { + pub fn heterogeneous(elements: impl IntoIterator) -> Self { FixedLengthTuple::from_elements(elements).into() } /// Returns the variable-length element of this tuple, if it has one. - pub(crate) fn variable_element(&self) -> Option<&T> + pub fn variable_element(&self) -> Option<&T> where T: Copy, { @@ -1279,7 +1279,7 @@ impl Tuple { } /// Returns an iterator of all of the fixed-length element types of this tuple. - pub(crate) fn fixed_elements(&self) -> impl Iterator + '_ { + pub fn fixed_elements(&self) -> impl Iterator + '_ { match self { Tuple::Fixed(tuple) => Either::Left(tuple.all_elements().iter()), Tuple::Variable(tuple) => Either::Right(tuple.fixed_elements()), @@ -1288,21 +1288,21 @@ impl Tuple { /// Returns an iterator of all of the element types of this tuple. Does not deduplicate the /// elements, and does not distinguish between fixed- and variable-length elements. - pub(crate) fn all_elements(&self) -> &[T] { + pub fn all_elements(&self) -> &[T] { match self { Tuple::Fixed(tuple) => tuple.all_elements(), Tuple::Variable(tuple) => tuple.all_elements(), } } - pub(crate) fn iter_all_elements(&self) -> impl DoubleEndedIterator + '_ + pub fn iter_all_elements(&self) -> impl DoubleEndedIterator + '_ where T: Copy, { self.all_elements().iter().copied() } - pub(crate) fn into_all_elements_with_kind(self) -> impl Iterator> { + pub fn into_all_elements_with_kind(self) -> impl Iterator> { match self { Tuple::Fixed(tuple) => Either::Left(tuple.into_all_elements_with_kind()), Tuple::Variable(tuple) => Either::Right(tuple.into_all_elements_with_kind()), @@ -1310,14 +1310,14 @@ impl Tuple { } /// Returns the length of this tuple. - pub(crate) fn len(&self) -> TupleLength { + pub fn len(&self) -> TupleLength { match self { Tuple::Fixed(tuple) => TupleLength::Fixed(tuple.len()), Tuple::Variable(tuple) => tuple.len(), } } - pub(crate) fn truthiness(&self) -> Truthiness { + pub fn truthiness(&self) -> Truthiness { match self.len().size_hint() { // The tuple type is AlwaysFalse if it contains only the empty tuple (_, Some(0)) => Truthiness::AlwaysFalse, @@ -1330,14 +1330,14 @@ impl Tuple { } impl<'db> Tuple> { - pub(crate) fn homogeneous_element_type(&self, db: &'db dyn Db) -> Type<'db> { + pub fn homogeneous_element_type(&self, db: &'db dyn Db) -> Type<'db> { UnionType::from_elements_leave_aliases(db, self.all_elements()) } /// Resizes this tuple to a different length, if possible. If this tuple cannot satisfy the /// desired minimum or maximum length, we return an error. If we return an `Ok` result, the /// [`len`][Self::len] of the resulting tuple is guaranteed to be equal to `new_length`. - pub(crate) fn resize( + pub fn resize( &self, db: &'db dyn Db, new_length: TupleLength, @@ -1348,7 +1348,7 @@ impl<'db> Tuple> { } } - pub(super) fn recursive_type_normalized_impl( + pub fn recursive_type_normalized_impl( &self, db: &'db dyn Db, div: Type<'db>, @@ -1364,7 +1364,7 @@ impl<'db> Tuple> { } } - pub(crate) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -1429,7 +1429,7 @@ impl<'db> Tuple> { } } - pub(super) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( &self, db: &'db dyn Db, other: &Self, @@ -1557,7 +1557,7 @@ impl<'db> Tuple> { } } - pub(crate) fn is_single_valued(&self, db: &'db dyn Db) -> bool { + pub fn is_single_valued(&self, db: &'db dyn Db) -> bool { match self { Tuple::Fixed(tuple) => tuple.is_single_valued(db), Tuple::Variable(_) => false, @@ -1571,7 +1571,7 @@ impl<'db> Tuple> { /// For variable-length tuples, this yields all pairs of elements that could overlap at runtime, /// including prefix/suffix elements matched by position, and variable elements that could /// align with any position in the other tuple. - pub(crate) fn try_for_each_element_pair(&self, other: &Self, mut f: F) -> Result<(), E> + pub fn try_for_each_element_pair(&self, other: &Self, mut f: F) -> Result<(), E> where F: FnMut(Type<'db>, Type<'db>) -> Result<(), E>, { @@ -1707,7 +1707,7 @@ impl<'db> Tuple> { } /// Return the `TupleSpec` for the singleton `sys.version_info` - pub(crate) fn version_info_spec(db: &'db dyn Db) -> TupleSpec<'db> { + pub fn version_info_spec(db: &'db dyn Db) -> TupleSpec<'db> { let python_version = Program::get(db).python_version(db); let int_instance_ty = KnownClass::Int.to_instance(db); @@ -1758,7 +1758,7 @@ impl<'db> PyIndex<'db> for &Tuple> { } } -pub(crate) enum TupleElement { +pub enum TupleElement { Fixed(T), Prefix(T), Variable(T), @@ -1772,13 +1772,13 @@ pub(crate) enum TupleElement { /// unpack the values from a rhs tuple into those targets. If the rhs is a union, call /// `unpack_tuple` separately for each element of the union. We will automatically wrap the types /// assigned to the starred target in `list`. -pub(crate) struct TupleUnpacker<'db> { +pub struct TupleUnpacker<'db> { db: &'db dyn Db, targets: Tuple>, } impl<'db> TupleUnpacker<'db> { - pub(crate) fn new(db: &'db dyn Db, len: TupleLength) -> Self { + pub fn new(db: &'db dyn Db, len: TupleLength) -> Self { let new_builders = |len: usize| std::iter::repeat_with(|| UnionBuilder::new(db)).take(len); let targets = match len { TupleLength::Fixed(len) => { @@ -1800,10 +1800,7 @@ impl<'db> TupleUnpacker<'db> { /// identical. The lengths only have to be identical if both sides are fixed-length; if either /// side is variable-length, we will pull multiple values out of the rhs variable-length /// portion, and assign multiple values to the starred target, as needed. - pub(crate) fn unpack_tuple( - &mut self, - values: &Tuple>, - ) -> Result<(), ResizeTupleError> { + pub fn unpack_tuple(&mut self, values: &Tuple>) -> Result<(), ResizeTupleError> { let values = values.resize(self.db, self.targets.len())?; match (&mut self.targets, &values) { (Tuple::Fixed(targets), Tuple::Fixed(values)) => { @@ -1821,7 +1818,7 @@ impl<'db> TupleUnpacker<'db> { /// [`unpack_tuple`][TupleUnpacker::unpack_tuple] multiple times, each target type will be the /// union of the type unpacked into that target from each of the rhs tuples. If there is a /// starred target, we will each unpacked type in `list`. - pub(crate) fn into_types(self) -> impl Iterator> { + pub fn into_types(self) -> impl Iterator> { self.targets .into_all_elements_with_kind() .map(|builder| match builder { @@ -1865,14 +1862,14 @@ impl<'db> VariableLengthTuple> { } #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum ResizeTupleError { +pub enum ResizeTupleError { TooFewValues, TooManyValues, } /// A builder for creating a new [`TupleSpec`] #[derive(Clone)] -pub(crate) enum TupleSpecBuilder<'db> { +pub enum TupleSpecBuilder<'db> { Fixed(Vec>), Variable { prefix: Vec>, @@ -1882,11 +1879,11 @@ pub(crate) enum TupleSpecBuilder<'db> { } impl<'db> TupleSpecBuilder<'db> { - pub(crate) fn with_capacity(capacity: usize) -> Self { + pub fn with_capacity(capacity: usize) -> Self { TupleSpecBuilder::Fixed(Vec::with_capacity(capacity)) } - pub(crate) fn push(&mut self, element: Type<'db>) { + pub fn push(&mut self, element: Type<'db>) { match self { TupleSpecBuilder::Fixed(elements) => elements.push(element), TupleSpecBuilder::Variable { suffix, .. } => suffix.push(element), @@ -1894,7 +1891,7 @@ impl<'db> TupleSpecBuilder<'db> { } /// Concatenates another tuple to the end of this tuple, returning a new tuple. - pub(crate) fn concat(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { + pub fn concat(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { match (&mut self, other) { (TupleSpecBuilder::Fixed(left_tuple), TupleSpec::Fixed(right_tuple)) => { left_tuple.extend_from_slice(&right_tuple.0); @@ -1969,7 +1966,7 @@ impl<'db> TupleSpecBuilder<'db> { /// `tuple[int, str, bytes]`, the result will be a tuple-spec builder for /// `tuple[int | str | bytes, ...]`. We could consider improving this in the future if real-world /// use cases arise. - pub(crate) fn union(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { + pub fn union(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { match (&mut self, other) { (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) if our_elements.len() == new_elements.len() => @@ -2008,7 +2005,7 @@ impl<'db> TupleSpecBuilder<'db> { /// For example, if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a /// tuple-spec for `tuple[object, object]`, the result will be a tuple-spec builder for /// `tuple[int, str]` (since `int & object` simplifies to `int`, and `str & object` to `str`). - pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Option { + pub fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Option { match (&mut self, other) { // Both fixed-length with the same length: element-wise intersection. (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) @@ -2070,7 +2067,7 @@ impl<'db> TupleSpecBuilder<'db> { } } - pub(super) fn build(self) -> TupleSpec<'db> { + pub fn build(self) -> TupleSpec<'db> { match self { TupleSpecBuilder::Fixed(elements) => { TupleSpec::Fixed(FixedLengthTuple(elements.into_boxed_slice())) diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs index d8203f680e3db..af4d64c8b484f 100644 --- a/crates/ty_python_semantic/src/types/type_alias.rs +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -24,13 +24,13 @@ pub struct PEP695TypeAliasType<'db> { rhs_scope: ScopeId<'db>, - pub(super) specialization: Option>, + pub specialization: Option>, } // The Salsa heap is tracked separately. impl get_size2::GetSize for PEP695TypeAliasType<'_> {} -pub(super) fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, type_alias: PEP695TypeAliasType<'db>, visitor: &V, @@ -40,14 +40,14 @@ pub(super) fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized #[salsa::tracked] impl<'db> PEP695TypeAliasType<'db> { - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + pub fn definition(self, db: &'db dyn Db) -> Definition<'db> { let scope = self.rhs_scope(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) } /// The RHS type of a PEP-695 style type alias with specialization applied. - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn value_type(self, db: &'db dyn Db) -> Type<'db> { self.apply_function_specialization(db, self.raw_value_type(db)) } @@ -80,7 +80,7 @@ impl<'db> PEP695TypeAliasType<'db> { } } - pub(crate) fn apply_specialization( + pub fn apply_specialization( self, db: &'db dyn Db, f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, @@ -105,12 +105,12 @@ impl<'db> PEP695TypeAliasType<'db> { } } - pub(crate) fn is_specialized(self, db: &'db dyn Db) -> bool { + pub fn is_specialized(self, db: &'db dyn Db) -> bool { self.specialization(db).is_some() } #[salsa::tracked(cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + pub fn generic_context(self, db: &'db dyn Db) -> Option> { let scope = self.rhs_scope(db); let file = scope.file(db); let parsed = parsed_module(db, file).load(db); @@ -142,7 +142,7 @@ pub struct ManualPEP695TypeAliasType<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for ManualPEP695TypeAliasType<'_> {} -pub(super) fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, type_alias: ManualPEP695TypeAliasType<'db>, visitor: &V, @@ -163,7 +163,7 @@ impl<'db> ManualPEP695TypeAliasType<'db> { }, heap_size=ruff_memory_usage::heap_size )] - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn value_type(self, db: &'db dyn Db) -> Type<'db> { let definition = self.definition(db); let file = definition.file(db); let module = parsed_module(db, file).load(db); @@ -190,7 +190,7 @@ pub enum TypeAliasType<'db> { ManualPEP695(ManualPEP695TypeAliasType<'db>), } -pub(super) fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, type_alias: TypeAliasType<'db>, visitor: &V, @@ -209,14 +209,14 @@ pub(super) fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } impl<'db> TypeAliasType<'db> { - pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { + pub fn name(self, db: &'db dyn Db) -> &'db str { match self { TypeAliasType::PEP695(type_alias) => type_alias.name(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.name(db), } } - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + pub fn definition(self, db: &'db dyn Db) -> Definition<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.definition(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.definition(db), @@ -230,21 +230,21 @@ impl<'db> TypeAliasType<'db> { } } - pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.raw_value_type(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), } } - pub(crate) fn as_pep_695_type_alias(self) -> Option> { + pub fn as_pep_695_type_alias(self) -> Option> { match self { TypeAliasType::PEP695(type_alias) => Some(type_alias), TypeAliasType::ManualPEP695(_) => None, } } - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + pub fn generic_context(self, db: &'db dyn Db) -> Option> { // TODO: Add support for generic non-PEP695 type aliases. match self { TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db), @@ -252,21 +252,21 @@ impl<'db> TypeAliasType<'db> { } } - pub(crate) fn specialization(self, db: &'db dyn Db) -> Option> { + pub fn specialization(self, db: &'db dyn Db) -> Option> { match self { TypeAliasType::PEP695(type_alias) => type_alias.specialization(db), TypeAliasType::ManualPEP695(_) => None, } } - pub(super) fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + pub fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.apply_function_specialization(db, ty), TypeAliasType::ManualPEP695(_) => ty, } } - pub(crate) fn apply_specialization( + pub fn apply_specialization( self, db: &'db dyn Db, f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, @@ -280,7 +280,7 @@ impl<'db> TypeAliasType<'db> { } /// Returns a struct that can display the fully qualified name of this type alias. - pub(crate) fn qualified_name(self, db: &'db dyn Db) -> QualifiedTypeAliasName<'db> { + pub fn qualified_name(self, db: &'db dyn Db) -> QualifiedTypeAliasName<'db> { QualifiedTypeAliasName::from_type_alias(db, self) } } @@ -290,13 +290,13 @@ impl<'db> TypeAliasType<'db> { // have the same components. You'd expect them to compare equal, but they'd compare // unequal if `PartialEq`/`Eq` were naively derived. #[derive(Clone, Copy)] -pub(crate) struct QualifiedTypeAliasName<'db> { +pub struct QualifiedTypeAliasName<'db> { db: &'db dyn Db, type_alias: TypeAliasType<'db>, } impl<'db> QualifiedTypeAliasName<'db> { - pub(crate) fn from_type_alias(db: &'db dyn Db, type_alias: TypeAliasType<'db>) -> Self { + pub fn from_type_alias(db: &'db dyn Db, type_alias: TypeAliasType<'db>) -> Self { Self { db, type_alias } } @@ -304,7 +304,7 @@ impl<'db> QualifiedTypeAliasName<'db> { /// /// For example, calling this method on a type alias `D` inside a class `C` in module `a.b` /// would return `["a", "b", "C"]`. - pub(crate) fn components_excluding_self(&self) -> Vec { + pub fn components_excluding_self(&self) -> Vec { let definition = self.type_alias.definition(self.db); let file = definition.file(self.db); let file_scope_id = definition.file_scope(self.db); diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index f97156e50b5b4..6e670aff06076 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -60,18 +60,18 @@ pub enum TypedDictType<'db> { } impl<'db> TypedDictType<'db> { - pub(crate) fn new(defining_class: ClassType<'db>) -> Self { + pub fn new(defining_class: ClassType<'db>) -> Self { Self::Class(defining_class) } - pub(crate) fn defining_class(self) -> Option> { + pub fn defining_class(self) -> Option> { match self { Self::Class(defining_class) => Some(defining_class), Self::Synthesized(_) => None, } } - pub(crate) fn items(self, db: &'db dyn Db) -> &'db TypedDictSchema<'db> { + pub fn items(self, db: &'db dyn Db) -> &'db TypedDictSchema<'db> { #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] fn class_based_items<'db>(db: &'db dyn Db, class: ClassType<'db>) -> TypedDictSchema<'db> { let Some((class_literal, specialization)) = class.static_class_literal(db) else { @@ -108,7 +108,7 @@ impl<'db> TypedDictType<'db> { } } - pub(crate) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -129,7 +129,7 @@ impl<'db> TypedDictType<'db> { // Subtyping between `TypedDict`s follows the algorithm described at: // https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub fn has_relation_to_impl<'c>( self, db: &'db dyn Db, target: TypedDictType<'db>, @@ -376,7 +376,7 @@ impl<'db> TypedDictType<'db> { /// be assignable to both.) /// /// TODO: Adding support for `closed` and `extra_items` will complicate this. - pub(crate) fn is_disjoint_from_impl<'c>( + pub fn is_disjoint_from_impl<'c>( self, db: &'db dyn Db, other: TypedDictType<'db>, @@ -466,7 +466,7 @@ impl<'db> TypedDictType<'db> { } } -pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, typed_dict: TypedDictType<'db>, visitor: &V, @@ -483,7 +483,7 @@ pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } } -pub(super) fn typed_dict_params_from_class_def(class_stmt: &StmtClassDef) -> TypedDictParams { +pub fn typed_dict_params_from_class_def(class_stmt: &StmtClassDef) -> TypedDictParams { let mut typed_dict_params = TypedDictParams::default(); // Check for `total` keyword argument in the class definition @@ -506,7 +506,7 @@ pub(super) fn typed_dict_params_from_class_def(class_stmt: &StmtClassDef) -> Typ } #[derive(Debug, Clone, Copy)] -pub(super) enum TypedDictAssignmentKind { +pub enum TypedDictAssignmentKind { /// For subscript assignments like `d["key"] = value` Subscript, /// For constructor arguments like `MyTypedDict(key=value)` @@ -534,21 +534,21 @@ impl TypedDictAssignmentKind { } /// A helper that validates assignments of a value to a specific key on a `TypedDict`. -pub(super) struct TypedDictKeyAssignment<'a, 'db, 'ast> { - pub(super) context: &'a InferContext<'db, 'ast>, - pub(super) typed_dict: TypedDictType<'db>, - pub(super) full_object_ty: Option>, - pub(super) key: &'a str, - pub(super) value_ty: Type<'db>, - pub(super) typed_dict_node: AnyNodeRef<'ast>, - pub(super) key_node: AnyNodeRef<'ast>, - pub(super) value_node: AnyNodeRef<'ast>, - pub(super) assignment_kind: TypedDictAssignmentKind, - pub(super) emit_diagnostic: bool, +pub struct TypedDictKeyAssignment<'a, 'db, 'ast> { + pub context: &'a InferContext<'db, 'ast>, + pub typed_dict: TypedDictType<'db>, + pub full_object_ty: Option>, + pub key: &'a str, + pub value_ty: Type<'db>, + pub typed_dict_node: AnyNodeRef<'ast>, + pub key_node: AnyNodeRef<'ast>, + pub value_node: AnyNodeRef<'ast>, + pub assignment_kind: TypedDictAssignmentKind, + pub emit_diagnostic: bool, } impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { - pub(super) fn validate(&self) -> bool { + pub fn validate(&self) -> bool { let db = self.context.db(); let items = self.typed_dict.items(db); @@ -694,7 +694,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { /// Reports errors for any keys that are required but not provided. /// /// Returns true if the assignment is valid, or false otherwise. -pub(super) fn validate_typed_dict_required_keys<'db, 'ast>( +pub fn validate_typed_dict_required_keys<'db, 'ast>( context: &InferContext<'db, 'ast>, typed_dict: TypedDictType<'db>, provided_keys: &OrderSet, @@ -809,7 +809,7 @@ fn extract_typed_dict_keys<'db>( } } -pub(super) fn validate_typed_dict_constructor<'db, 'ast>( +pub fn validate_typed_dict_constructor<'db, 'ast>( context: &InferContext<'db, 'ast>, typed_dict: TypedDictType<'db>, arguments: &'ast Arguments, @@ -985,7 +985,7 @@ fn validate_from_keywords<'db, 'ast>( /// Validates a `TypedDict` dictionary literal assignment, /// e.g. `person: Person = {"name": "Alice", "age": 30}` -pub(super) fn validate_typed_dict_dict_literal<'db>( +pub fn validate_typed_dict_dict_literal<'db>( context: &InferContext<'db, '_>, typed_dict: TypedDictType<'db>, dict_expr: &ast::ExprDict, @@ -1034,14 +1034,14 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct SynthesizedTypedDictType<'db> { #[returns(ref)] - pub(crate) items: TypedDictSchema<'db>, + pub items: TypedDictSchema<'db>, } // The Salsa heap is tracked separately. impl get_size2::GetSize for SynthesizedTypedDictType<'_> {} impl<'db> SynthesizedTypedDictType<'db> { - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -1098,25 +1098,25 @@ impl<'db> FromIterator<(Name, TypedDictField<'db>)> for TypedDictSchema<'db> { #[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)] pub struct TypedDictField<'db> { - pub(super) declared_ty: Type<'db>, + pub declared_ty: Type<'db>, flags: TypedDictFieldFlags, first_declaration: Option>, } impl<'db> TypedDictField<'db> { - pub(crate) const fn is_required(&self) -> bool { + pub const fn is_required(&self) -> bool { self.flags.contains(TypedDictFieldFlags::REQUIRED) } - pub(crate) const fn is_read_only(&self) -> bool { + pub const fn is_read_only(&self) -> bool { self.flags.contains(TypedDictFieldFlags::READ_ONLY) } - pub(crate) const fn first_declaration(&self) -> Option> { + pub const fn first_declaration(&self) -> Option> { self.first_declaration } - pub(crate) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -1133,14 +1133,14 @@ impl<'db> TypedDictField<'db> { } } -pub(super) struct TypedDictFieldBuilder<'db> { +pub struct TypedDictFieldBuilder<'db> { declared_ty: Type<'db>, flags: TypedDictFieldFlags, first_declaration: Option>, } impl<'db> TypedDictFieldBuilder<'db> { - pub(crate) fn new(declared_ty: Type<'db>) -> Self { + pub fn new(declared_ty: Type<'db>) -> Self { Self { declared_ty, flags: TypedDictFieldFlags::empty(), @@ -1148,22 +1148,22 @@ impl<'db> TypedDictFieldBuilder<'db> { } } - pub(crate) fn required(mut self, yes: bool) -> Self { + pub fn required(mut self, yes: bool) -> Self { self.flags.set(TypedDictFieldFlags::REQUIRED, yes); self } - pub(crate) fn read_only(mut self, yes: bool) -> Self { + pub fn read_only(mut self, yes: bool) -> Self { self.flags.set(TypedDictFieldFlags::READ_ONLY, yes); self } - pub(crate) fn first_declaration(mut self, definition: Option>) -> Self { + pub fn first_declaration(mut self, definition: Option>) -> Self { self.first_declaration = definition; self } - pub(crate) fn build(self) -> TypedDictField<'db> { + pub fn build(self) -> TypedDictField<'db> { TypedDictField { declared_ty: self.declared_ty, flags: self.flags, diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index bca9d7f8e43c7..1b8083fb83ddb 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -21,26 +21,22 @@ use crate::{ }; impl<'db> Type<'db> { - pub(crate) const fn is_type_var(self) -> bool { + pub const fn is_type_var(self) -> bool { matches!(self, Type::TypeVar(_)) } - pub(crate) const fn as_typevar(self) -> Option> { + pub const fn as_typevar(self) -> Option> { match self { Type::TypeVar(bound_typevar) => Some(bound_typevar), _ => None, } } - pub(crate) fn has_typevar(self, db: &'db dyn Db) -> bool { + pub fn has_typevar(self, db: &'db dyn Db) -> bool { any_over_type(db, self, false, |ty| matches!(ty, Type::TypeVar(_))) } - pub(crate) fn references_typevar( - self, - db: &'db dyn Db, - typevar_id: TypeVarIdentity<'db>, - ) -> bool { + pub fn references_typevar(self, db: &'db dyn Db, typevar_id: TypeVarIdentity<'db>) -> bool { any_over_type(db, self, false, |ty| match ty { Type::TypeVar(bound_typevar) => typevar_id == bound_typevar.typevar(db).identity(db), Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { @@ -50,7 +46,7 @@ impl<'db> Type<'db> { }) } - pub(crate) fn has_non_self_typevar(self, db: &'db dyn Db) -> bool { + pub fn has_non_self_typevar(self, db: &'db dyn Db) -> bool { any_over_type( db, self, @@ -59,7 +55,7 @@ impl<'db> Type<'db> { ) } - pub(crate) fn has_typevar_or_typevar_instance(self, db: &'db dyn Db) -> bool { + pub fn has_typevar_or_typevar_instance(self, db: &'db dyn Db) -> bool { any_over_type(db, self, false, |ty| { matches!( ty, @@ -68,7 +64,7 @@ impl<'db> Type<'db> { }) } - pub(crate) fn has_unspecialized_type_var(self, db: &'db dyn Db) -> bool { + pub fn has_unspecialized_type_var(self, db: &'db dyn Db) -> bool { any_over_type(db, self, false, |ty| { matches!(ty, Type::Dynamic(DynamicType::UnspecializedTypeVar)) }) @@ -110,7 +106,7 @@ impl<'db> Type<'db> { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct TypeVarInstance<'db> { /// The identity of this typevar - pub(crate) identity: TypeVarIdentity<'db>, + pub identity: TypeVarIdentity<'db>, /// The upper bound or constraint on the type of this TypeVar, if any. Don't use this field /// directly; use the `bound_or_constraints` (or `upper_bound` and `constraints`) methods @@ -118,7 +114,7 @@ pub struct TypeVarInstance<'db> { _bound_or_constraints: Option>, /// The explicitly specified variance of the TypeVar - pub(super) explicit_variance: Option, + pub explicit_variance: Option, /// The default type for this TypeVar, if any. Don't use this field directly, use the /// `default_type` method instead (to evaluate any lazy default). @@ -128,7 +124,7 @@ pub struct TypeVarInstance<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for TypeVarInstance<'_> {} -pub(super) fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, typevar: TypeVarInstance<'db>, visitor: &V, @@ -160,7 +156,7 @@ pub(super) fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( #[salsa::tracked] impl<'db> TypeVarInstance<'db> { - pub(crate) fn with_binding_context( + pub fn with_binding_context( self, db: &'db dyn Db, binding_context: Definition<'db>, @@ -178,7 +174,7 @@ impl<'db> TypeVarInstance<'db> { ) } - pub(super) fn with_identity(self, db: &'db dyn Db, identity: TypeVarIdentity<'db>) -> Self { + pub fn with_identity(self, db: &'db dyn Db, identity: TypeVarIdentity<'db>) -> Self { Self::new( db, identity, @@ -188,11 +184,11 @@ impl<'db> TypeVarInstance<'db> { ) } - pub(crate) fn name(self, db: &'db dyn Db) -> &'db Name { + pub fn name(self, db: &'db dyn Db) -> &'db Name { self.identity(db).name(db) } - pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + pub fn definition(self, db: &'db dyn Db) -> Option> { self.identity(db).definition(db) } @@ -200,15 +196,15 @@ impl<'db> TypeVarInstance<'db> { self.identity(db).kind(db) } - pub(crate) fn is_self(self, db: &'db dyn Db) -> bool { + pub fn is_self(self, db: &'db dyn Db) -> bool { matches!(self.kind(db), TypeVarKind::TypingSelf) } - pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { + pub fn is_paramspec(self, db: &'db dyn Db) -> bool { self.kind(db).is_paramspec() } - pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { + pub fn upper_bound(self, db: &'db dyn Db) -> Option> { if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { Some(ty) } else { @@ -216,7 +212,7 @@ impl<'db> TypeVarInstance<'db> { } } - pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&'db [Type<'db>]> { + pub fn constraints(self, db: &'db dyn Db) -> Option<&'db [Type<'db>]> { if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { Some(tuple.elements(db)) } else { @@ -224,10 +220,7 @@ impl<'db> TypeVarInstance<'db> { } } - pub(crate) fn bound_or_constraints( - self, - db: &'db dyn Db, - ) -> Option> { + pub fn bound_or_constraints(self, db: &'db dyn Db) -> Option> { self._bound_or_constraints(db).and_then(|w| match w { TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { Some(bound_or_constraints) @@ -243,15 +236,12 @@ impl<'db> TypeVarInstance<'db> { /// Returns the bounds or constraints of this typevar. If the typevar is unbounded, returns /// `object` as its upper bound. - pub(crate) fn require_bound_or_constraints( - self, - db: &'db dyn Db, - ) -> TypeVarBoundOrConstraints<'db> { + pub fn require_bound_or_constraints(self, db: &'db dyn Db) -> TypeVarBoundOrConstraints<'db> { self.bound_or_constraints(db) .unwrap_or_else(|| TypeVarBoundOrConstraints::UpperBound(Type::object())) } - pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { + pub fn default_type(self, db: &'db dyn Db) -> Option> { let visitor = TypeVarDefaultVisitor::new(None); self.default_type_impl(db, &visitor) } @@ -643,17 +633,17 @@ impl<'db> TypeVarInstance<'db> { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct BoundTypeVarInstance<'db> { pub typevar: TypeVarInstance<'db>, - pub(super) binding_context: BindingContext<'db>, + pub binding_context: BindingContext<'db>, /// If [`Some`], this indicates that this type variable is the `args` or `kwargs` component /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. - pub(super) paramspec_attr: Option, + pub paramspec_attr: Option, } // The Salsa heap is tracked separately. impl get_size2::GetSize for BoundTypeVarInstance<'_> {} impl<'db> BoundTypeVarInstance<'db> { - pub(crate) fn with_name_suffix(self, db: &'db dyn Db, suffix: &str) -> Self { + pub fn with_name_suffix(self, db: &'db dyn Db, suffix: &str) -> Self { Self::new( db, self.typevar(db).with_name_suffix(db, suffix), @@ -666,7 +656,7 @@ impl<'db> BoundTypeVarInstance<'db> { /// /// This is used for comparing whether two bound typevars represent the same logical typevar, /// regardless of e.g. differences in their bounds or constraints due to materialization. - pub(crate) fn identity(self, db: &'db dyn Db) -> BoundTypeVarIdentity<'db> { + pub fn identity(self, db: &'db dyn Db) -> BoundTypeVarIdentity<'db> { BoundTypeVarIdentity { identity: self.typevar(db).identity(db), binding_context: self.binding_context(db), @@ -674,15 +664,15 @@ impl<'db> BoundTypeVarInstance<'db> { } } - pub(crate) fn name(self, db: &'db dyn Db) -> &'db Name { + pub fn name(self, db: &'db dyn Db) -> &'db Name { self.typevar(db).name(db) } - pub(crate) fn kind(self, db: &'db dyn Db) -> TypeVarKind { + pub fn kind(self, db: &'db dyn Db) -> TypeVarKind { self.typevar(db).kind(db) } - pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { + pub fn is_paramspec(self, db: &'db dyn Db) -> bool { self.kind(db).is_paramspec() } @@ -694,7 +684,7 @@ impl<'db> BoundTypeVarInstance<'db> { /// /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` /// type variable. - pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { + pub fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { debug_assert!( self.is_paramspec(db), "Expected a ParamSpec, got {:?}", @@ -726,7 +716,7 @@ impl<'db> BoundTypeVarInstance<'db> { /// /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` /// type variable. - pub(crate) fn without_paramspec_attr(self, db: &'db dyn Db) -> Self { + pub fn without_paramspec_attr(self, db: &'db dyn Db) -> Self { debug_assert!( self.is_paramspec(db), "Expected a ParamSpec, got {:?}", @@ -749,13 +739,13 @@ impl<'db> BoundTypeVarInstance<'db> { /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g. /// differences in their bounds or constraints due to materialization. - pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { + pub fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { self.identity(db) == other.identity(db) } /// Create a new PEP 695 type variable that can be used in signatures /// of synthetic generic functions. - pub(crate) fn synthetic(db: &'db dyn Db, name: Name, variance: TypeVarVariance) -> Self { + pub fn synthetic(db: &'db dyn Db, name: Name, variance: TypeVarVariance) -> Self { let identity = TypeVarIdentity::new( db, name, @@ -773,7 +763,7 @@ impl<'db> BoundTypeVarInstance<'db> { } /// Create a new synthetic `Self` type variable with the given upper bound. - pub(crate) fn synthetic_self( + pub fn synthetic_self( db: &'db dyn Db, upper_bound: Type<'db>, binding_context: BindingContext<'db>, @@ -796,7 +786,7 @@ impl<'db> BoundTypeVarInstance<'db> { /// Returns an identical type variable with its `TypeVarBoundOrConstraints` mapped by the /// provided closure. - pub(crate) fn map_bound_or_constraints( + pub fn map_bound_or_constraints( self, db: &'db dyn Db, f: impl FnOnce(Option>) -> Option>, @@ -818,7 +808,7 @@ impl<'db> BoundTypeVarInstance<'db> { ) } - pub(crate) fn variance_with_polarity( + pub fn variance_with_polarity( self, db: &'db dyn Db, polarity: TypeVarVariance, @@ -839,7 +829,7 @@ impl<'db> BoundTypeVarInstance<'db> { self.variance_with_polarity(db, TypeVarVariance::Covariant) } - pub(super) fn apply_type_mapping_impl<'a>( + pub fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -916,7 +906,7 @@ impl<'db> BoundTypeVarInstance<'db> { } } -pub(super) fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>, visitor: &V, @@ -947,7 +937,7 @@ impl<'db> BoundTypeVarInstance<'db> { /// By using `U` in the generic class, it becomes bound, and so we have a /// `BoundTypeVarInstance`. As part of binding `U` we must also bind its default value /// (resulting in `T@C`). - pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { + pub fn default_type(self, db: &'db dyn Db) -> Option> { bound_typevar_default_type(db, self) } @@ -966,7 +956,7 @@ impl<'db> BoundTypeVarInstance<'db> { ) } - pub(super) fn to_instance(self, db: &'db dyn Db) -> Option { + pub fn to_instance(self, db: &'db dyn Db) -> Option { Some(Self::new( db, self.typevar(db).to_instance(db)?, @@ -995,11 +985,11 @@ pub enum TypeVarKind { } impl TypeVarKind { - pub(super) const fn is_self(self) -> bool { + pub const fn is_self(self) -> bool { matches!(self, Self::TypingSelf) } - pub(super) const fn is_paramspec(self) -> bool { + pub const fn is_paramspec(self) -> bool { matches!(self, Self::ParamSpec | Self::Pep695ParamSpec) } } @@ -1013,13 +1003,13 @@ impl TypeVarKind { pub struct TypeVarIdentity<'db> { /// The name of this TypeVar (e.g. `T`) #[returns(ref)] - pub(crate) name: Name, + pub name: Name, /// The type var's definition (None if synthesized) - pub(crate) definition: Option>, + pub definition: Option>, /// The kind of typevar (PEP 695, Legacy, or TypingSelf) - pub(crate) kind: TypeVarKind, + pub kind: TypeVarKind, } impl get_size2::GetSize for TypeVarIdentity<'_> {} @@ -1097,14 +1087,14 @@ impl<'db> From> for BindingContext<'db> { } impl<'db> BindingContext<'db> { - pub(crate) fn definition(self) -> Option> { + pub fn definition(self) -> Option> { match self { BindingContext::Definition(definition) => Some(definition), BindingContext::Synthetic => None, } } - pub(super) fn name(self, db: &'db dyn Db) -> Option { + pub fn name(self, db: &'db dyn Db) -> Option { self.definition().and_then(|definition| definition.name(db)) } } @@ -1132,11 +1122,11 @@ impl std::fmt::Display for ParamSpecAttrKind { /// have been materialized differently. #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] pub struct BoundTypeVarIdentity<'db> { - pub(crate) identity: TypeVarIdentity<'db>, - pub(crate) binding_context: BindingContext<'db>, + pub identity: TypeVarIdentity<'db>, + pub binding_context: BindingContext<'db>, /// If [`Some`], this indicates that this type variable is the `args` or `kwargs` component /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. - pub(super) paramspec_attr: Option, + pub paramspec_attr: Option, } #[salsa::tracked( @@ -1206,7 +1196,7 @@ impl<'db> From> for TypeVarBoundOrConstraintsEval #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct TypeVarConstraints<'db> { #[returns(ref)] - pub(super) elements: Box<[Type<'db>]>, + pub elements: Box<[Type<'db>]>, } impl get_size2::GetSize for TypeVarConstraints<'_> {} @@ -1222,7 +1212,7 @@ fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } impl<'db> TypeVarConstraints<'db> { - pub(super) fn as_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn as_type(self, db: &'db dyn Db) -> Type<'db> { UnionType::from_elements(db, self.elements(db)) } @@ -1237,11 +1227,7 @@ impl<'db> TypeVarConstraints<'db> { )) } - pub(super) fn map( - self, - db: &'db dyn Db, - transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, - ) -> Self { + pub fn map(self, db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Type<'db>) -> Self { let mapped = self .elements(db) .iter() @@ -1250,7 +1236,7 @@ impl<'db> TypeVarConstraints<'db> { TypeVarConstraints::new(db, mapped) } - pub(crate) fn map_with_boundness_and_qualifiers( + pub fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, @@ -1351,7 +1337,7 @@ pub enum TypeVarBoundOrConstraints<'db> { Constraints(TypeVarConstraints<'db>), } -pub(super) fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>( +pub fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, bounds: TypeVarBoundOrConstraints<'db>, visitor: &V, @@ -1387,6 +1373,6 @@ impl<'db> TypeVarBoundOrConstraints<'db> { } /// A [`CycleDetector`] that is used in `TypeVarInstance::default_type`. -pub(crate) type TypeVarDefaultVisitor<'db> = +pub type TypeVarDefaultVisitor<'db> = CycleDetector, Option>>; -pub(crate) struct VisitTypeVarDefault; +pub struct VisitTypeVarDefault; diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index eddd3aab0b9aa..0db11e29505c7 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -16,17 +16,13 @@ use super::context::InferContext; use super::diagnostic::INVALID_ASSIGNMENT; /// Unpacks the value expression type to their respective targets. -pub(crate) struct Unpacker<'db, 'ast> { +pub struct Unpacker<'db, 'ast> { context: InferContext<'db, 'ast>, targets: FxHashMap>, } impl<'db, 'ast> Unpacker<'db, 'ast> { - pub(crate) fn new( - db: &'db dyn Db, - target_scope: ScopeId<'db>, - module: &'ast ParsedModuleRef, - ) -> Self { + pub fn new(db: &'db dyn Db, target_scope: ScopeId<'db>, module: &'ast ParsedModuleRef) -> Self { Self { context: InferContext::new(db, target_scope, module), targets: FxHashMap::default(), @@ -42,7 +38,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { } /// Unpack the value to the target expression. - pub(crate) fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) { + pub fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) { debug_assert!( matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)), "Unpacking target must be a list or tuple expression" @@ -178,7 +174,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { } } - pub(crate) fn finish(mut self) -> UnpackResult<'db> { + pub fn finish(mut self) -> UnpackResult<'db> { self.targets.shrink_to_fit(); UnpackResult { @@ -190,7 +186,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { } #[derive(Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)] -pub(crate) struct UnpackResult<'db> { +pub struct UnpackResult<'db> { targets: FxHashMap>, diagnostics: TypeCheckDiagnostics, @@ -209,17 +205,14 @@ impl<'db> UnpackResult<'db> { /// May panic if a scoped expression ID is passed in that does not correspond to a sub- /// expression of the target. #[track_caller] - pub(crate) fn expression_type(&self, expr_id: impl Into) -> Type<'db> { + pub fn expression_type(&self, expr_id: impl Into) -> Type<'db> { self.try_expression_type(expr_id).expect( "expression should belong to this `UnpackResult` and \ `Unpacker` should have inferred a type for it", ) } - pub(crate) fn try_expression_type( - &self, - expr: impl Into, - ) -> Option> { + pub fn try_expression_type(&self, expr: impl Into) -> Option> { self.targets .get(&expr.into()) .copied() @@ -227,11 +220,11 @@ impl<'db> UnpackResult<'db> { } /// Returns the diagnostics in this unpacking assignment. - pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics { + pub fn diagnostics(&self) -> &TypeCheckDiagnostics { &self.diagnostics } - pub(crate) fn cycle_initial(cycle_recovery: Type<'db>) -> Self { + pub fn cycle_initial(cycle_recovery: Type<'db>) -> Self { Self { targets: FxHashMap::default(), diagnostics: TypeCheckDiagnostics::default(), @@ -239,7 +232,7 @@ impl<'db> UnpackResult<'db> { } } - pub(crate) fn cycle_normalized( + pub fn cycle_normalized( mut self, db: &'db dyn Db, previous_cycle_result: &UnpackResult<'db>, diff --git a/crates/ty_python_semantic/src/types/variance.rs b/crates/ty_python_semantic/src/types/variance.rs index aa32bd6f177a7..a872e65297cec 100644 --- a/crates/ty_python_semantic/src/types/variance.rs +++ b/crates/ty_python_semantic/src/types/variance.rs @@ -19,7 +19,7 @@ impl TypeVarVariance { // supremum #[must_use] - pub(crate) const fn join(self, other: Self) -> Self { + pub const fn join(self, other: Self) -> Self { use TypeVarVariance::{Bivariant, Contravariant, Covariant, Invariant}; match (self, other) { (Invariant, _) | (_, Invariant) => Invariant, @@ -49,14 +49,14 @@ impl TypeVarVariance { /// We would say `ConstantInt[str]` = `ConstantInt[float]`, so we qualify as /// using semantic equivalence. #[must_use] - pub(crate) fn compose(self, other: Self) -> Self { + pub fn compose(self, other: Self) -> Self { self.compose_thunk(|| other) } /// Like `compose`, but takes `other` as a thunk to avoid unnecessary /// computation when `self` is `Bivariant`. #[must_use] - pub(crate) fn compose_thunk(self, other: F) -> Self + pub fn compose_thunk(self, other: F) -> Self where F: FnOnce() -> Self, { @@ -77,7 +77,7 @@ impl TypeVarVariance { /// Flips the polarity of the variance. /// /// Covariant becomes contravariant, contravariant becomes covariant, others remain unchanged. - pub(crate) const fn flip(self) -> Self { + pub const fn flip(self) -> Self { match self { TypeVarVariance::Invariant => TypeVarVariance::Invariant, TypeVarVariance::Covariant => TypeVarVariance::Contravariant, @@ -86,14 +86,14 @@ impl TypeVarVariance { } } - pub(crate) const fn is_covariant(self) -> bool { + pub const fn is_covariant(self) -> bool { matches!( self, TypeVarVariance::Covariant | TypeVarVariance::Bivariant ) } - pub(crate) const fn is_contravariant(self) -> bool { + pub const fn is_contravariant(self) -> bool { matches!( self, TypeVarVariance::Contravariant | TypeVarVariance::Bivariant @@ -121,7 +121,7 @@ impl std::iter::FromIterator for TypeVarVariance { } } -pub(crate) trait VarianceInferable<'db>: Sized { +pub trait VarianceInferable<'db>: Sized { /// The variance of `typevar` in `self` /// /// Generally, one will implement this by traversing any types within `self` @@ -153,7 +153,7 @@ pub(crate) trait VarianceInferable<'db>: Sized { } } -pub(crate) struct WithPolarity { +pub struct WithPolarity { variance_inferable: T, polarity: TypeVarVariance, } diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index ea9cb64b21348..0b17285568d05 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -30,7 +30,7 @@ use std::cell::{Cell, RefCell}; /// The trait does not guard against infinite recursion out of the box, /// but it makes it easy for implementors of the trait to do so. /// See [`any_over_type`] for an example of how to do this. -pub(crate) trait TypeVisitor<'db> { +pub trait TypeVisitor<'db> { /// Should the visitor trigger inference of and visit lazily-inferred type attributes? fn should_visit_lazy_type_attributes(&self) -> bool; @@ -123,7 +123,7 @@ pub(crate) trait TypeVisitor<'db> { /// Enumeration of types that may contain other types, such as unions, intersections, and generics. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub(super) enum NonAtomicType<'db> { +pub enum NonAtomicType<'db> { Union(UnionType<'db>), Intersection(IntersectionType<'db>), FunctionLiteral(FunctionType<'db>), @@ -145,7 +145,7 @@ pub(super) enum NonAtomicType<'db> { NewTypeInstance(NewType<'db>), } -pub(super) enum TypeKind<'db> { +pub enum TypeKind<'db> { Atomic, NonAtomic(NonAtomicType<'db>), } @@ -215,7 +215,7 @@ impl<'db> From> for TypeKind<'db> { } } -pub(super) fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( +pub fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, non_atomic_type: NonAtomicType<'db>, visitor: &V, @@ -259,7 +259,7 @@ pub(super) fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( } } -pub(crate) fn walk_type_with_recursion_guard<'db>( +pub fn walk_type_with_recursion_guard<'db>( db: &'db dyn Db, ty: Type<'db>, visitor: &impl TypeVisitor<'db>, @@ -278,10 +278,10 @@ pub(crate) fn walk_type_with_recursion_guard<'db>( } #[derive(Default, Debug)] -pub(crate) struct TypeCollector<'db>(RefCell>>); +pub struct TypeCollector<'db>(RefCell>>); impl<'db> TypeCollector<'db> { - pub(crate) fn type_was_already_seen(&self, ty: Type<'db>) -> bool { + pub fn type_was_already_seen(&self, ty: Type<'db>) -> bool { !self.0.borrow_mut().insert(ty) } } @@ -345,7 +345,7 @@ where /// The `should_visit_lazy_type_attributes` parameter controls whether deferred type attributes /// (value of a type alias, attributes of a class-based protocol, bounds/constraints of a typevar) /// are visited or not. -pub(super) fn any_over_type<'db>( +pub fn any_over_type<'db>( db: &'db dyn Db, ty: Type<'db>, should_visit_lazy_type_attributes: bool, @@ -367,7 +367,7 @@ pub(super) fn any_over_type<'db>( /// The `should_visit_lazy_type_attributes` parameter controls whether deferred type attributes /// (value of a type alias, attributes of a class-based protocol, bounds/constraints of a typevar) /// are visited or not. -pub(super) fn find_over_type<'db, T>( +pub fn find_over_type<'db, T>( db: &'db dyn Db, ty: Type<'db>, should_visit_lazy_type_attributes: bool, diff --git a/crates/ty_python_semantic/src/unpack.rs b/crates/ty_python_semantic/src/unpack.rs index c9acc3fcfd95f..15736d6f158a5 100644 --- a/crates/ty_python_semantic/src/unpack.rs +++ b/crates/ty_python_semantic/src/unpack.rs @@ -28,51 +28,47 @@ use crate::types::EvaluationMode; /// * a field of a type that is a return type of a cross-module query /// * an argument of a cross-module query #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] -pub(crate) struct Unpack<'db> { - pub(crate) file: File, +pub struct Unpack<'db> { + pub file: File, - pub(crate) value_file_scope: FileScopeId, + pub value_file_scope: FileScopeId, - pub(crate) target_file_scope: FileScopeId, + pub target_file_scope: FileScopeId, /// The target expression that is being unpacked. For example, in `(a, b) = (1, 2)`, the target /// expression is `(a, b)`. #[no_eq] #[tracked] #[returns(ref)] - pub(crate) _target: AstNodeRef, + pub _target: AstNodeRef, /// The ingredient representing the value expression of the unpacking. For example, in /// `(a, b) = (1, 2)`, the value expression is `(1, 2)`. - pub(crate) value: UnpackValue<'db>, + pub value: UnpackValue<'db>, } // The Salsa heap is tracked separately. impl get_size2::GetSize for Unpack<'_> {} impl<'db> Unpack<'db> { - pub(crate) fn target<'ast>( - self, - db: &'db dyn Db, - parsed: &'ast ParsedModuleRef, - ) -> &'ast ast::Expr { + pub fn target<'ast>(self, db: &'db dyn Db, parsed: &'ast ParsedModuleRef) -> &'ast ast::Expr { self._target(db).node(parsed) } /// Returns the scope where the unpack target expression belongs to. - pub(crate) fn target_scope(self, db: &'db dyn Db) -> ScopeId<'db> { + pub fn target_scope(self, db: &'db dyn Db) -> ScopeId<'db> { self.target_file_scope(db).to_scope_id(db, self.file(db)) } /// Returns the range of the unpack target expression. - pub(crate) fn range(self, db: &'db dyn Db, module: &ParsedModuleRef) -> TextRange { + pub fn range(self, db: &'db dyn Db, module: &ParsedModuleRef) -> TextRange { self.target(db, module).range() } } /// The expression that is being unpacked. #[derive(Clone, Copy, Debug, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) struct UnpackValue<'db> { +pub struct UnpackValue<'db> { /// The kind of unpack expression kind: UnpackKind, /// The expression we are unpacking @@ -80,17 +76,17 @@ pub(crate) struct UnpackValue<'db> { } impl<'db> UnpackValue<'db> { - pub(crate) fn new(kind: UnpackKind, expression: Expression<'db>) -> Self { + pub fn new(kind: UnpackKind, expression: Expression<'db>) -> Self { Self { kind, expression } } /// Returns the underlying [`Expression`] that is being unpacked. - pub(crate) const fn expression(self) -> Expression<'db> { + pub const fn expression(self) -> Expression<'db> { self.expression } /// Returns the expression as an [`AnyNodeRef`]. - pub(crate) fn as_any_node_ref<'ast>( + pub fn as_any_node_ref<'ast>( self, db: &'db dyn Db, module: &'ast ParsedModuleRef, @@ -98,13 +94,13 @@ impl<'db> UnpackValue<'db> { self.expression().node_ref(db, module).into() } - pub(crate) const fn kind(self) -> UnpackKind { + pub const fn kind(self) -> UnpackKind { self.kind } } #[derive(Clone, Copy, Debug, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) enum UnpackKind { +pub enum UnpackKind { /// An iterable expression like the one in a `for` loop or a comprehension. Iterable { mode: EvaluationMode }, /// An context manager expression like the one in a `with` statement. @@ -115,7 +111,7 @@ pub(crate) enum UnpackKind { /// The position of the target element in an unpacking. #[derive(Clone, Copy, Debug, Hash, PartialEq, salsa::Update, get_size2::GetSize)] -pub(crate) enum UnpackPosition { +pub enum UnpackPosition { /// The target element is in the first position of the unpacking. First, /// The target element is in the position other than the first position of the unpacking.