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. 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"] ) }) { 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 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}",