From 6e0e49eda88118f69b5521dd8138dd09a384e5ee Mon Sep 17 00:00:00 2001 From: Louis Maddox Date: Sat, 6 Dec 2025 18:19:04 +0000 Subject: [PATCH 1/4] Add minimal-size build profile (#21826) This PR adds the same `minimal-size` profile as `uv` repo workspace has ```toml # Profile to build a minimally sized binary for uv-build [profile.minimal-size] inherits = "release" opt-level = "z" # This will still show a panic message, we only skip the unwind panic = "abort" codegen-units = 1 ``` but removes its `panic = "abort"` setting - As discussed in #21825 Compared to the ones pre-built via `uv tool install`, this builds 35% smaller ruff and 24% smaller ty binaries (as measured [here](https://github.com/lmmx/just-pre-commit/blob/master/refresh_binaries.sh)) --- Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e948f46085..554ad219d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -272,6 +272,12 @@ large_stack_arrays = "allow" lto = "fat" codegen-units = 16 +# Profile to build a minimally sized binary for ruff/ty +[profile.minimal-size] +inherits = "release" +opt-level = "z" +codegen-units = 1 + # Some crates don't change as much but benefit more from # more expensive optimization passes, so we selectively # decrease codegen-units in some cases. From cbff09b9aff3f06845d7cc39a7af37f207981c98 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Sun, 7 Dec 2025 16:10:46 +0530 Subject: [PATCH 2/4] [flake8-bandit] Fix false positive when using non-standard `CSafeLoader` path (S506). (#21830) --- .../ruff_linter/resources/test/fixtures/flake8_bandit/S506.py | 2 ++ .../src/rules/flake8_bandit/rules/unsafe_yaml_load.rs | 1 + 2 files changed, 3 insertions(+) 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 9fd87de3e3..b316ca8aea 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S506.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S506.py @@ -28,9 +28,11 @@ yaml.load("{}", SafeLoader) yaml.load("{}", yaml.SafeLoader) yaml.load("{}", CSafeLoader) yaml.load("{}", yaml.CSafeLoader) +yaml.load("{}", yaml.cyaml.CSafeLoader) yaml.load("{}", NewSafeLoader) yaml.load("{}", Loader=SafeLoader) yaml.load("{}", Loader=yaml.SafeLoader) yaml.load("{}", Loader=CSafeLoader) yaml.load("{}", Loader=yaml.CSafeLoader) +yaml.load("{}", Loader=yaml.cyaml.CSafeLoader) yaml.load("{}", Loader=NewSafeLoader) 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 593f5e01da..bc5e846f99 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 @@ -75,6 +75,7 @@ pub(crate) fn unsafe_yaml_load(checker: &Checker, call: &ast::ExprCall) { qualified_name.segments(), ["yaml", "SafeLoader" | "CSafeLoader"] | ["yaml", "loader", "SafeLoader" | "CSafeLoader"] + | ["yaml", "cyaml", "CSafeLoader"] ) }) { From 285d6410d3dcc0567fee1d72bc46bfe3008af36b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 7 Dec 2025 09:27:14 -0500 Subject: [PATCH 3/4] [ty] Avoid double-analyzing tuple in `Final` subscript (#21828) ## Summary As-is, a single-element tuple gets destructured via: ```rust let arguments = if let ast::Expr::Tuple(tuple) = slice { &*tuple.elts } else { std::slice::from_ref(slice) }; ``` But then, because it's a single element, we call `infer_annotation_expression_impl`, passing in the tuple, rather than the first element. Closes https://github.com/astral-sh/ty/issues/1793. Closes https://github.com/astral-sh/ty/issues/1768. --------- Co-authored-by: Claude Opus 4.5 --- .../mdtest/type_qualifiers/classvar.md | 31 +++++++++++++++++++ .../resources/mdtest/type_qualifiers/final.md | 16 ++++++++++ .../mdtest/type_qualifiers/initvar.md | 19 ++++++++++++ .../infer/builder/annotation_expression.rs | 20 +++++++----- 4 files changed, 78 insertions(+), 8 deletions(-) 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 f697543c1b..5578332677 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -101,6 +101,37 @@ class C: x: ClassVar[int, str] = 1 ``` +## Trailing comma creates a tuple + +A trailing comma in a subscript creates a single-element tuple. We need to handle this gracefully +and emit a proper error rather than crashing (see +[ty#1793](https://github.com/astral-sh/ty/issues/1793)). + +```py +from typing import ClassVar + +class C: + # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?" + x: ClassVar[(),] + +# error: [invalid-attribute-access] "Cannot assign to ClassVar `x` from an instance of type `C`" +C().x = 42 +reveal_type(C.x) # revealed: Unknown +``` + +This also applies when the trailing comma is inside the brackets (see +[ty#1768](https://github.com/astral-sh/ty/issues/1768)): + +```py +from typing import ClassVar + +class D: + # A trailing comma here doesn't change the meaning; it's still one argument. + a: ClassVar[int,] = 1 + +reveal_type(D.a) # revealed: int +``` + ## 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 1c42ded723..af468f6132 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -340,6 +340,22 @@ class C: x: Final[int, str] = 1 ``` +### Trailing comma creates a tuple + +A trailing comma in a subscript creates a single-element tuple. We need to handle this gracefully +and emit a proper error rather than crashing (see +[ty#1793](https://github.com/astral-sh/ty/issues/1793)). + +```py +from typing import Final + +# error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?" +x: Final[(),] = 42 + +# error: [invalid-assignment] "Reassignment of `Final` symbol `x` is not allowed" +x = 56 +``` + ### Illegal `Final` in type expression ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md index 9282782ce4..7d6c14b1c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/initvar.md @@ -112,6 +112,25 @@ class Wrong: x: InitVar[int, str] # error: [invalid-type-form] "Type qualifier `InitVar` expected exactly 1 argument, got 2" ``` +A trailing comma in a subscript creates a single-element tuple. We need to handle this gracefully +and emit a proper error rather than crashing (see +[ty#1793](https://github.com/astral-sh/ty/issues/1793)). + +```py +from dataclasses import InitVar, dataclass + +@dataclass +class AlsoWrong: + # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?" + x: InitVar[(),] + +# revealed: (self: AlsoWrong, x: Unknown) -> None +reveal_type(AlsoWrong.__init__) + +# error: [unresolved-attribute] +reveal_type(AlsoWrong(42).x) # revealed: Unknown +``` + A bare `InitVar` is not allowed according to the [type annotation grammar]: ```py 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 ce3b2a4a92..374f65878b 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 @@ -273,10 +273,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { std::slice::from_ref(slice) }; - let num_arguments = arguments.len(); - let type_and_qualifiers = if num_arguments == 1 { - let mut type_and_qualifiers = self - .infer_annotation_expression_impl(slice, PEP613Policy::Disallowed); + let type_and_qualifiers = if let [argument] = arguments { + let mut type_and_qualifiers = self.infer_annotation_expression_impl( + argument, + PEP613Policy::Disallowed, + ); match type_qualifier { SpecialFormType::ClassVar => { @@ -307,6 +308,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { 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}", @@ -325,10 +327,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { std::slice::from_ref(slice) }; - let num_arguments = arguments.len(); - let type_and_qualifiers = if num_arguments == 1 { - let mut type_and_qualifiers = self - .infer_annotation_expression_impl(slice, PEP613Policy::Disallowed); + let type_and_qualifiers = if let [argument] = arguments { + let mut type_and_qualifiers = self.infer_annotation_expression_impl( + argument, + PEP613Policy::Disallowed, + ); type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR); type_and_qualifiers } else { @@ -341,6 +344,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let num_arguments = arguments.len(); builder.into_diagnostic(format_args!( "Type qualifier `InitVar` expected exactly 1 argument, \ got {num_arguments}", From 857fd4f6837060626a5a2cc5a1e00355e78b9970 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 7 Dec 2025 15:58:11 +0000 Subject: [PATCH 4/4] [ty] Add test case for fixed panic (#21832) --- .../corpus/future_annotations_recursive_annotation.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/ty_python_semantic/resources/corpus/future_annotations_recursive_annotation.py diff --git a/crates/ty_python_semantic/resources/corpus/future_annotations_recursive_annotation.py b/crates/ty_python_semantic/resources/corpus/future_annotations_recursive_annotation.py new file mode 100644 index 0000000000..0e14c6a985 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/future_annotations_recursive_annotation.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +class MyClass: + type: type = str